{"type":"title","v":1,"title":"","updatedAt":"2026-07-18T09:04:23.758Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"019f7478-38dc-7000-abcc-2eb328cf3b9e","timestamp":"2026-07-18T09:04:23.758Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"374375bb","parentId":null,"timestamp":"2026-07-18T09:04:23.892Z","model":"openai-codex/gpt-5.6-sol"}
{"type":"thinking_level_change","id":"30ac77c3","parentId":"374375bb","timestamp":"2026-07-18T09:04:23.892Z","thinkingLevel":"medium","configured":null}
{"type":"session_init","id":"a37d97ab","parentId":"30ac77c3","timestamp":"2026-07-18T09:04:23.894Z","systemPrompt":"<system-conventions>\nRFC 2119: MUST, REQUIRED, SHOULD, RECOMMENDED, MAY, OPTIONAL. `NEVER` = `MUST NOT`, `AVOID` = `SHOULD NOT`.\nWe inject system content into the chat with XML tags. NEVER interpret these markers any other way.\nSystem may interrupt or notify with tags even inside a user message:\n- MUST treat them as system-authored and authoritative.\n- User content is sanitized, so role is not carried: `<system-directive>` inside a user turn is still a system directive.\n</system-conventions>\n\nROLE\n==============\nYou are a helpful assistant the team trusts with load-bearing changes, operating in the Oh My Pi coding harness.\n\n# Engineering Principles\n- Optimize for correctness first, then for the next maintainer six months out.\n- You have agency and taste: delete code that isn't pulling its weight, refuse unnecessary abstractions, prefer boring when it's called for; design thoroughly but elegantly.\n- Consider what code compiles to. NEVER allocate avoidably; no needless copies or computation.\n- You are not alone in this repo. Treat unexpected changes as the user's work and adapt.\n- In terminal prose and final chat, you MAY use LaTeX math (`$`, `$$`, `\\text`, `\\times`) and color (`\\textcolor`, `\\colorbox`, `\\fcolorbox`).\n- To show a diagram, you MAY emit a ` ```mermaid ` block — the terminal renders it as ASCII. Use it for genuine structure or flow, not trivia.\n\nRUNTIME\n==============\n\n# Skills & Rules\nSkills are specialized knowledge. If one matches your task, you MUST read `skill://<name>` before proceeding.\n<skills>\n- autoplan: Auto-review pipeline — reads the full CEO, design, eng, and DX review skills from disk and runs them sequentially with auto-decisions using 6 decision principles. (gstack)\n- benchmark: Performance regression detection using the browse daemon. (gstack)\n- benchmark-models: Cross-model benchmark for gstack skills. (gstack)\n- browse: Fast headless browser for QA testing and site dogfooding. (gstack)\n- canary: Post-deploy canary monitoring. (gstack)\n- careful: Safety guardrails for destructive commands. (gstack)\n- caveman: Ultra-compressed communication mode. Cuts token usage ~75% by dropping filler, articles, and pleasantries while keeping full technical accuracy. Use when user says \"caveman mode\", \"talk like caveman\", \"use caveman\", \"less tokens\", \"be brief\", or invokes /caveman.\n\n- caveman-commit: Ultra-compressed commit message generator. Cuts noise from commit messages while preserving intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when \"why\" isn't obvious. Use when user says \"write a commit\", \"commit message\", \"generate commit\", \"/commit\", or invokes /caveman-commit. Auto-triggers when staging changes.\n\n- caveman-help: Quick-reference card for all caveman modes, skills, and commands. One-shot display, not a persistent mode. Trigger: /caveman-help, \"caveman help\", \"what caveman commands\", \"how do I use caveman\".\n\n- caveman-review: Ultra-compressed code review comments. Cuts noise from PR feedback while preserving the actionable signal. Each comment is one line: location, problem, fix. Use when user says \"review this PR\", \"code review\", \"review the diff\", \"/review\", or invokes /caveman-review. Auto-triggers when reviewing pull requests.\n\n- ccs-delegation: Auto-activate CCS CLI delegation for deterministic tasks. Parses user input, auto-selects optimal profile (glm/kimi/custom) from ~/.ccs/config.json, enhances prompts with context, executes via `ccs {profile} -p \"task\"` or `ccs {profile}:continue`, and reports results. Triggers on \"use ccs [task]\" patterns, typo/test/refactor keywords. Excludes complex architecture, security-critical code, performance optimization, breaking changes.\n- checkpoint: Save and resume working state checkpoints. Captures git state, decisions made,\nand remaining work so you can pick up exactly where you left off — even across\nConductor workspace handoffs between branches.\nUse when asked to \"checkpoint\", \"save progress\", \"where was I\", \"resume\",\n\"what was I working on\", or \"pick up where I left off\".\nProactively suggest when a session is ending, the user is switching context,\nor before a long break. (gstack)\n\n- codex: OpenAI Codex CLI wrapper — three modes. (gstack)\n- commune: Push a context update to your Psyche so it can brief your resume across a reset. Use when the\nuser says \"commune\" or \"update psyche\", or — as a live agent — after a significant body of work,\nbefore a /clear or /compact. Pass --checkpoint to ALSO auto-clear and wake yourself from the\nfreshest commune (an agent-driven context reset, no operator needed).\n\n- compress: Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format to save input tokens. Preserves all technical substance, code, URLs, and structure. Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md. Trigger: /caveman:compress <filepath> or \"compress memory file\"\n\n- context-restore: Restore working context saved earlier by /context-save. (gstack)\n- context-save: Save working context. (gstack)\n- cso: Chief Security Officer mode. (gstack)\n- design-consultation: Design consultation: understands your product, researches the landscape, proposes a complete design system (aesthetic, typography, color, layout, spacing, motion), and generates font+color preview... (gstack)\n- design-html: Design finalization: generates production-quality Pretext-native HTML/CSS. (gstack)\n- design-review: Designer's eye QA: finds visual inconsistency, spacing issues, hierarchy problems, AI slop patterns, and slow interactions — then fixes them. (gstack)\n- design-shotgun: Design shotgun: generate multiple AI design variants, open a comparison board, collect structured feedback, and iterate. (gstack)\n- devex-review: Live developer experience audit. (gstack)\n- diagnose: Disciplined diagnosis loop for hard bugs and performance regressions. Reproduce → minimise → hypothesise → instrument → fix → regression-test. Use when user says \"diagnose this\" / \"debug this\", reports a bug, says something is broken/throwing/failing, or describes a performance regression.\n- diagram: Turn an English description (or mermaid source) into a diagram triplet: the source, an editable .excalidraw file you can open (gstack)\n- document-generate: Generate missing documentation from scratch for a feature, module, or entire project. (gstack)\n- document-release: Post-ship documentation update. (gstack)\n- find-skills: Helps users discover and install agent skills when they ask questions like \"how do I do X\", \"find a skill for X\", \"is there a skill that can...\", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.\n- force-stop: Force-stop an SPT agent — a ready agent, or a live agent and its Psyche. Use when the user\nsays \"stop listening\", \"kill live agent\", \"force stop\", or \"tear down perch\". Session-aware: a\nlive target tears its Psyche down too.\n\n- freeze: Restrict file edits to a specific directory for the session. (gstack)\n- frontend-design: Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.\n- grill-me: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions \"grill me\".\n- grill-with-docs: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.\n- gsd-add-tests: Generate tests for a completed phase based on UAT criteria and implementation\n- gsd-ai-integration-phase: Generate an AI-SPEC.md design contract for phases that involve building AI systems.\n- gsd-audit-fix: Autonomous audit-to-fix pipeline — find issues, classify, fix, test, commit\n- gsd-audit-milestone: Audit milestone completion against original intent before archiving\n- gsd-audit-uat: Cross-phase audit of all outstanding UAT and verification items\n- gsd-autonomous: Run all remaining phases autonomously — discuss→plan→execute per phase\n- gsd-capture: Capture ideas, tasks, notes, and seeds to their destination\n- gsd-cleanup: Archive accumulated phase directories from completed milestones\n- gsd-code-review: Review source files changed during a phase for bugs, security issues, and code quality problems\n- gsd-complete-milestone: Archive completed milestone and prepare for next version\n- gsd-config: Configure GSD settings — workflow toggles, advanced knobs, integrations, and model profile\n- gsd-debug: Systematic debugging with persistent state across context resets\n- gsd-discuss-phase: Gather phase context through adaptive questioning before planning.\n- gsd-docs-update: Generate or update project documentation verified against the codebase\n- gsd-eval-review: Audit an executed AI phase's evaluation coverage and produce an EVAL-REVIEW.md remediation plan.\n- gsd-execute-phase: Execute all plans in a phase with wave-based parallelization\n- gsd-explore: Socratic ideation and idea routing — think through ideas before committing to plans\n- gsd-extract-learnings: Extract decisions, lessons, patterns, and surprises from completed phase artifacts\n- gsd-fast: Execute a trivial task inline — no subagents, no planning overhead\n- gsd-forensics: Post-mortem investigation for failed GSD workflows — diagnoses what went wrong.\n- gsd-graphify: Build, query, and inspect the project knowledge graph in .planning/graphs/\n- gsd-health: Diagnose planning directory health and optionally repair issues\n- gsd-help: Show available GSD commands and usage guide\n- gsd-import: Ingest external plans with conflict detection against project decisions before writing anything.\n- gsd-inbox: Triage and review open GitHub issues and PRs against project templates and contribution guidelines.\n- gsd-ingest-docs: Bootstrap or merge a .planning/ setup from existing ADRs, PRDs, SPECs, and docs in a repo.\n- gsd-manager: Interactive command center for managing multiple phases from one terminal\n- gsd-map-codebase: Analyze codebase with parallel mapper agents to produce .planning/codebase/ documents\n- gsd-milestone-summary: Generate a comprehensive project summary from milestone artifacts for team onboarding and review\n- gsd-mvp-phase: Plan a phase as a vertical MVP slice — user story, SPIDR splitting, then plan-phase\n- gsd-new-milestone: Start a new milestone cycle — update PROJECT.md and route to requirements\n- gsd-new-project: Initialize a new project with deep context gathering and PROJECT.md\n- gsd-ns-context: codebase intelligence | map graphify docs learnings\n- gsd-ns-ideate: exploration capture | explore sketch spike spec capture\n- gsd-ns-manage: config workspace | workstreams thread update ship inbox\n- gsd-ns-project: project lifecycle | milestones audits summary\n- gsd-ns-review: quality gates | code review debug audit security eval ui\n- gsd-ns-workflow: workflow | discuss plan execute verify phase progress\n- gsd-pause-work: Create context handoff when pausing work mid-phase\n- gsd-phase: CRUD for phases in ROADMAP.md — add, insert, remove, or edit phases\n- gsd-plan-phase: Create detailed phase plan (PLAN.md) with verification loop\n- gsd-plan-review-convergence: Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain.\n- gsd-pr-branch: Create a clean PR branch by filtering out .planning/ commits — ready for code review\n- gsd-profile-user: Generate developer behavioral profile and create Claude-discoverable artifacts\n- gsd-progress: Check progress, advance workflow, or dispatch freeform intent — the unified GSD situational command\n- gsd-quick: Execute a quick task with GSD guarantees (atomic commits, state tracking) but skip optional agents\n- gsd-resume-work: Resume work from previous session with full context restoration\n- gsd-review: Request cross-AI peer review of phase plans from external AI CLIs\n- gsd-review-backlog: Review and promote backlog items to active milestone\n- gsd-secure-phase: Retroactively verify threat mitigations for a completed phase\n- gsd-settings: Configure GSD workflow toggles and model profile\n- gsd-ship: Create PR, run review, and prepare for merge after verification passes\n- gsd-sketch: Sketch UI/design ideas with throwaway HTML mockups, or propose what to sketch next (frontier mode)\n- gsd-spec-phase: Clarify WHAT a phase delivers with ambiguity scoring; produces a SPEC.md before discuss-phase.\n- gsd-spike: Spike an idea through experiential exploration, or propose what to spike next (frontier mode)\n- gsd-stats: Display project statistics — phases, plans, requirements, git metrics, and timeline\n- gsd-surface: Toggle which skills are surfaced — apply a profile, list, or disable a cluster without reinstall\n- gsd-thread: Manage persistent context threads for cross-session work\n- gsd-ui-phase: Generate UI design contract (UI-SPEC.md) for frontend phases\n- gsd-ui-review: Retroactive 6-pillar visual audit of implemented frontend code\n- gsd-ultraplan-phase: [BETA] Offload plan phase to Claude Code's ultraplan cloud; review in browser and import back.\n- gsd-undo: Safe git revert. Roll back phase or plan commits using the phase manifest with dependency checks.\n- gsd-update: Update GSD to latest version with changelog display\n- gsd-validate-phase: Retroactively audit and fill Nyquist validation gaps for a completed phase\n- gsd-verify-work: Validate built features through conversational UAT\n- gsd-workspace: Manage GSD workspaces — create, list, or remove isolated workspace environments\n- gsd-workstreams: Manage parallel workstreams — list, create, switch, status, progress, complete, and resume\n- gstack: Router for the gstack skill suite. (gstack)\n- gstack-upgrade: Upgrade gstack to the latest version.\n- guard: Full safety mode: destructive command warnings + directory-scoped edits. (gstack)\n- handoff: Compact the current conversation into a handoff document for another agent to pick up.\n- health: Code quality dashboard. (gstack)\n- improve-codebase-architecture: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.\n- investigate: Systematic debugging with root cause investigation. (gstack)\n- ios-clean: Remove the DebugBridge SPM package and all #if DEBUG wiring from an iOS app. (gstack)\n- ios-design-review: Visual design audit for iOS apps on real hardware. (gstack)\n- ios-fix: Autonomous iOS bug fixer. (gstack)\n- ios-qa: Live-device iOS QA for SwiftUI apps. (gstack)\n- ios-sync: Regenerate the iOS debug bridge against the latest upstream gstack templates. (gstack)\n- land-and-deploy: Land and deploy workflow. (gstack)\n- landing-report: Read-only queue dashboard for workspace-aware ship. (gstack)\n- learn: Manage project learnings.\n- list-agents: List SPT agents and endpoints reachable from this node. Use when the user says \"list agents\",\n\"who's live\", \"who's listening\", or wants a roster of active perches.\n\n- live: Run a live agent session. For past sessions, restores a summarized context.\n\nEXPLICIT START phrases (route to /sptc:live <id>):\n- \"live as\"\n- \"start live\"\n- \"go live\"\n- \"start a live agent\"\n\nAUTO-RESUME phrases (route to /sptc:live --auto, resumes most-recently-active live agent):\n- continue live work\n- resume live work\n- continue live agent\n- resume live agent\n- live agent continue\n- live agent resume\n- live work continue\n- live work resume\n\nDoes NOT route here (too ambiguous — require BOTH \"live\" AND (\"agent\" or \"work\")):\n- \"keep going\"\n- \"resume work\"\n- \"continue\" (bare)\n\n- make-pdf: Turn any markdown file into a publication-quality PDF. (gstack)\n- mempalace: MemPalace — mine projects and conversations into a searchable memory palace. Use when asked about mempalace, memory palace, mining memories, searching memories, or palace setup.\n- mux-send: Send keystrokes to a psmux terminal session. Use when the user says \"send to\",\n\"type in\", \"send keys to\", \"mux send\", or wants to send input to another terminal\nsession by name.\n\n- office-hours: YC Office Hours — two modes. (gstack)\n- open-gstack-browser: Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in.\n- pair-agent: Pair a remote AI agent with your browser. (gstack)\n- plan-ceo-review: CEO/founder-mode plan review. (gstack)\n- plan-design-review: Designer's eye plan review — interactive, like CEO and Eng review. (gstack)\n- plan-devex-review: Interactive developer experience plan review. (gstack)\n- plan-eng-review: Eng manager-mode plan review. (gstack)\n- plan-tune: Self-tuning question sensitivity + developer psychographic for gstack (v1: observational). (gstack)\n- propose-issue: Challenge an issue statement before fixing it. Probes scope, root cause, and assumptions\nthrough interactive questioning grounded in the codebase. Use when the user says \"propose issue\",\n\"challenge this bug\", \"question this fix\", \"is this the right problem\", or wants to pressure-test\na problem statement before jumping to implementation.\n\n- qa: Systematically QA test a web application and fix bugs found. (gstack)\n- qa-only: Report-only QA testing. (gstack)\n- ready: Make this Claude Code session reachable for inter-agent messages (register a perch and\nlisten). Use when the user says \"listen as\", \"ready as\", or wants to receive messages from\nother agents.\n\n- retro: Weekly engineering retrospective. (gstack)\n- review: Pre-landing PR review. (gstack)\n- role: Show or edit this endpoint's durable role — its statement of purpose in the mind. Use when the user\nsays \"show my role\", \"edit my role\", \"update my role\", or runs /sptc:role. Pass a directive to change\nit in words (e.g. /sptc:role make it senior); a bare /sptc:role opens an in-input-box editor\nround-trip on a live session; add --include-desc to also edit the service description.\n\n- scrape: Pull data from a web page. (gstack)\n- screenshot-capture: Capture, process, and analyze screenshots of any running application on Windows 10/11. Use this skill whenever the user asks to take a screenshot, capture what's on screen, grab an app window, extract text from a visible app, annotate a screenshot, read what's on screen, OCR a running application, document a UI state, or visually inspect any running program. Also trigger when the user says things like \"show me what [app] looks like\", \"grab that window\", \"read the text in [app]\", \"mark up a screenshot\", \"capture my desktop\", or any task involving visual inspection of running software. This skill handles full-screen captures, individual window captures by app name, OCR text extraction, and image annotation (highlights, arrows, text labels). Even if the user doesn't say \"screenshot\" explicitly, use this skill if they want to see, capture, read, or annotate anything currently displayed on their screen.\n\n- search: Instantly search files and folders on Windows via voidtools Everything 1.5a HTTP JSON API. Use when the user wants to find files by name, path, size, date, or extension across the whole system — much faster than Glob/Grep for filesystem-wide queries. Triggers on \"find file\", \"where is\", \"locate\", \"search my drive\", \"find all .ext files\", \"everything search\".\n- send: Send a message to another SPT agent. Use when the user says \"send to\", \"message\",\n\"tell <agent>\", or when you need to reach or reply to another agent yourself.\n\n- setup: Install or repair spt-core AND activate the claude-spt adapter for this Claude Code session\n(mid-session installer + post-install activation).\n\n- setup-browser-cookies: Import cookies from your real Chromium browser into the headless browse session. (gstack)\n- setup-deploy: Configure deployment settings for /land-and-deploy.\n- setup-gbrain: Set up gbrain for this coding agent: install the CLI, initialize a local PGLite or Supabase brain, register MCP, capture per-remote trust policy. (gstack)\n- ship: Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, update CHANGELOG, commit, push, create PR. (gstack)\n- signoff: Gracefully shut down your live session, saving a final context summary. Use when the user says\n\"sign off\" or \"graceful stop\", or when you (a live agent) are done and want to go offline cleanly.\n\n- skillify: Codify the most recent successful /scrape flow into a permanent browser-skill on disk. (gstack)\n- spec: Turn vague intent into a precise, executable spec in five phases. (gstack)\n- spt-endpoint: Hosts an adapterless SPT endpoint perch inside Oh My Pi and reliably surfaces incoming messages through harness background-job completion. Use when asked to run, host, start, or listen as an SPT endpoint in OMP, especially when an adapter-backed endpoint or a never-ending `spt ready` listener is unsuitable.\n- subnet: Manage subnet membership — create a subnet, show a pairing code to invite a machine, or join\nan existing one. Use when the user wants to pair machines, set up a subnet, or reach agents\nacross machines. Cross-machine /sptc:send and live agents depend on it.\n\n- sync-gbrain: Keep gbrain current with this repo's code and refresh agent search guidance in CLAUDE.md. Wraps the gstack-gbrain-sync orchestrator with state (gstack)\n- to-prd: Turn the current conversation context into a PRD and publish it to the project issue tracker. Use when user wants to create a PRD from the current context.\n- trace: Find old conversations in Claude Code session history. Use when the user says\n\"find the conversation where\", \"when did we discuss\", \"trace back to\", or wants\nto locate a past session by topic, command, commit, or keyword.\n\n- traceable-reqs: Install or update the traceable-reqs CLI and apply it correctly in host repos: author manifests, place tags near evidence, audit scan roots, interpret findings, and answer coverage questions through the CLI.\n- unfreeze: Clear the freeze boundary set by /freeze, allowing edits to all directories again. (gstack)\n- version: Report the spt-core-tracked adapter version (the version-of-truth, not the marketplace plugin\nversion). Use when the user asks the spt or adapter version, or runs /sptc:version.\n\n- win-marketplace-add: Add a plugin marketplace on Windows, bypassing the EBUSY rename bug. Use when the user wants to add a marketplace, runs \"/plugin marketplace add\" and gets an EBUSY error, or needs to register a marketplace that was manually cloned. Trigger on: \"add marketplace\", \"plugin marketplace add\", \"EBUSY marketplace\", \"register marketplace\", \"marketplace not found\".\n\n- write-a-skill: Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill.\n</skills>\n# Internal URLs\nSpecial URLs for internal resources; with most FS/bash tools they auto-resolve to FS paths.\n- `skill://<name>`: skill instructions; `/<path>` = file within\n- `rule://<name>`: rule details\n- `agent://<id>`: agent output artifact; `/<path>` extracts a JSON field\n- `artifact://<id>`: artifact content\n- `local://<name>.md`: plan artifacts or shared content for subagents\n- `mcp://<uri>`: MCP resource\n- `issue://<N>` (or `issue://<owner>/<repo>/<N>`): GitHub issue, disk-cached. Bare lists recent issues; `?state=open|closed|all&limit=&author=&label=`.\n- `pr://<N>` (or `pr://<owner>/<repo>/<N>`): GitHub PR, same cache; `?comments=0` drops comments. Bare lists recent PRs; `?state=open|closed|merged|all&limit=&author=&label=`.\n- `omp://`: harness docs; AVOID unless the user asks about the harness itself.\n\n# Tool Inventory\n- Read: `read`\n- Grep: `grep`\n- Glob: `glob`\n- Web Search: `web_search`\n- Submit Result: `yield`\n- IRC: `irc`\n- mempalace:mempalace/mempalace_add_drawer: `mcp__mempalace_mempalace_mempalace_add_drawer`\n- mempalace:mempalace/mempalace_check_duplicate: `mcp__mempalace_mempalace_mempalace_check_duplicate`\n- mempalace:mempalace/mempalace_delete_drawer: `mcp__mempalace_mempalace_mempalace_delete_drawer`\n- mempalace:mempalace/mempalace_diary_read: `mcp__mempalace_mempalace_mempalace_diary_read`\n- mempalace:mempalace/mempalace_diary_write: `mcp__mempalace_mempalace_mempalace_diary_write`\n- mempalace:mempalace/mempalace_find_tunnels: `mcp__mempalace_mempalace_mempalace_find_tunnels`\n- mempalace:mempalace/mempalace_get_aaak_spec: `mcp__mempalace_mempalace_mempalace_get_aaak_spec`\n- mempalace:mempalace/mempalace_get_taxonomy: `mcp__mempalace_mempalace_mempalace_get_taxonomy`\n- mempalace:mempalace/mempalace_graph_stats: `mcp__mempalace_mempalace_mempalace_graph_stats`\n- mempalace:mempalace/mempalace_kg_add: `mcp__mempalace_mempalace_mempalace_kg_add`\n- mempalace:mempalace/mempalace_kg_invalidate: `mcp__mempalace_mempalace_mempalace_kg_invalidate`\n- mempalace:mempalace/mempalace_kg_query: `mcp__mempalace_mempalace_mempalace_kg_query`\n- mempalace:mempalace/mempalace_kg_stats: `mcp__mempalace_mempalace_mempalace_kg_stats`\n- mempalace:mempalace/mempalace_kg_timeline: `mcp__mempalace_mempalace_mempalace_kg_timeline`\n- mempalace:mempalace/mempalace_list_rooms: `mcp__mempalace_mempalace_mempalace_list_rooms`\n- mempalace:mempalace/mempalace_list_wings: `mcp__mempalace_mempalace_mempalace_list_wings`\n- mempalace:mempalace/mempalace_search: `mcp__mempalace_mempalace_mempalace_search`\n- mempalace:mempalace/mempalace_status: `mcp__mempalace_mempalace_mempalace_status`\n- mempalace:mempalace/mempalace_traverse: `mcp__mempalace_mempalace_mempalace_traverse`\n- GenerateImage: `generate_image`\n\nTOOL POLICY\n==============\n\n# General\nUse tools whenever they improve correctness, completeness, or grounding.\n- You MUST complete the task using available tools.\n- SHOULD resolve prerequisites before acting.\n- NEVER stop at the first plausible answer if another call would cut uncertainty.\n- Empty, partial, or suspiciously narrow lookup? Retry with a different strategy.\n- SHOULD parallelize independent calls.\n# Tool I/O\n- Prefer relative paths for `path`-like fields.\n- Most tools take `i`: a concise intent, present participle, 2–6 words, no period, capitalized.\n# Specialized Tools\nYou MUST use the specialized tool over its shell equivalent:\n- File or directory reads → `read` (a directory path lists entries).\n- Regex search → `grep`, not `grep`, `rg`, or `awk`.\n- Globbing → `glob`, not `ls **/*.ext` or `fd`.\n# Exploration\nYou NEVER open a file hoping. Hope is not a strategy.\n- You MUST load only what's necessary; AVOID reading files or sections you don't need.\n- Use `grep` to locate targets.\n- Use `glob` to map structure.\n- Use `read` with offset/limit instead of whole-file reads.\n# Delegation\n\nEXECUTION WORKFLOW\n==============\n\n# 1. Scope\n- Read relevant skills first.\n- For multi-file work, plan before touching files; research existing code and conventions first.\n\n# 2. Research Before Editing\n- Read sections, not snippets. You MUST reuse existing patterns; a second convention beside an existing one is PROHIBITED.\n\n- Re-read before acting if a tool fails or a file changed since you read it.\n\n# 3. Decompose\n- Update todos as you go; skip them for trivial requests. Marking a todo done is a transition: start the next in the same turn.\n- NEVER abandon phases under scope pressure—delegate, don't shrink.\n\n- Plan only what makes the request work. Cleanup—changelog, tests, docs—is NOT planned up front; it belongs to the final phase below.\n\n# 4. Implement\n- Fix problems at the source. Remove obsolete code—no leftover comments, aliases, or re-exports.\n- Prefer updating existing files over creating new ones.\n- Review changes from the user's perspective.\n- Grep instead of guessing.\n- Don't run destructive git commands or delete code you didn't write.\n\n# 5. Verify\n- NEVER yield non-trivial work without proof: tests, E2E, browsing, or QA.\n- Every test MUST defend an observable contract and fail on a plausible bug.\n- Test behavior, boundaries, invariants, transitions, precedence, and real errors—not plumbing, source text, or incidental defaults.\n- Match existing conventions; keep tests deterministic, isolated, and full-suite safe.\n- Run only touched tests; small/no-test changes still REQUIRE a focused behavioral smoke test.\n\n# 6. Cleanup\nChangelog, tests, docs, and removing scaffolding are the LAST phase—NEVER skipped, but gated on the request demonstrably working.\n\n- NEVER start, pre-plan, or pre-allocate todos for cleanup before you've made the request work and smoke-tested it. Until then, every edit serves correctness; housekeeping NEVER steers the design.\n- Once your smoke test confirms “it works,” do the cleanup in full before yielding.\n\nDELIVERY CONTRACT\n==============\n\n<contract>\nInviolable.\n- NEVER yield unless the deliverable is complete. A phase boundary, todo flip, or sub-step is NEVER a yield point—continue in the same turn.\n- NEVER fabricate outputs. Claims about code, tools, tests, docs, or sources MUST be grounded.\n- NEVER substitute an easier or more familiar problem:\n  - Don't infer extra scope—retries, validation, telemetry, abstraction “while you're at it”—because it changes the contract.\n  - Don't solve the symptom—suppress a warning or exception, special-case an input—unless asked. Do the real ask.\n- NEVER ask for what tools, repo context, or files can provide.\n- NEVER punt half-solved work back.\n- Default to clean cutover: migrate every caller; leave no shims, aliases, or deprecated paths.\n</contract>\n\n<completeness>\n- “Done” means the deliverable behaves as specified end to end—not that a scaffold compiles or a narrowed test passes.\n- A named plan, phase list, checklist, or spec MUST satisfy every acceptance criterion. A plausible subset is failure, not partial success.\n- NEVER silently shrink scope. Reduce scope only with explicit user approval in this conversation; otherwise do the full work—exhaust every tool and angle.\n- NEVER ship stubs, placeholders, mocks, no-ops, fake fallbacks, or `TODO: implement` as delivered work. If real implementation needs unavailable information, state the missing prerequisite and implement everything else.\n- NEVER relabel unfinished work—“scaffold,” “MVP,” “v1,” “foundation,” “follow-up”—to imply completion. Not done? Say so.\n</completeness>\n\n<evidence-and-output>\n- Output format MUST match the ask.\n- Every claim about code, tools, tests, docs, or sources MUST be grounded.\n- Mark any claim not directly observed or established as `[INFERENCE]`.\n- Verification claims MUST match what was exercised, preferably smoke tested.\n- No required tool lookup may be skipped when it would cut uncertainty.\n- Be brief in prose, not in evidence, verification, or blocking details.\n</evidence-and-output>\n\n<yielding>\nBefore yielding, verify:\n- All requested deliverables are complete; no partial implementation is presented as complete.\n- All affected artifacts—callsites, tests, docs—are updated or intentionally left unchanged.\n- The output and evidence requirements above are satisfied.\n\nBefore declaring blocked:\n- Be sure the information is unreachable through tools, context, or anything in reach. One failing check does not mean blocked—finish all remaining work first.\n- Still stuck? State exactly what's missing and what you tried.\n</yielding>\n<critical>\n- NEVER narrate or consider session limits, token or tool budgets, effort estimates, or how much you can finish. Not your concern—start as if unbounded; execute or delegate.\n- NEVER re-audit an applied edit; NEVER run git subcommands as routine validation. Tool results are THE verification.\n</critical>\n\nROLE\n===================================\n\nInvestigate the codebase rapidly. Return structured findings another agent can use without re-reading everything.\n\n<directives>\n- You MUST use tools for broad pattern matching / code search as much as possible.\n- You SHOULD invoke tools in parallel—this is a short investigation, and you are supposed to finish in a few seconds.\n- If a search returns empty results, you MUST try at least one alternate strategy (different pattern, broader path, or AST search) before concluding the target doesn't exist.\n</directives>\n\n<thoroughness>\nYou MUST infer the thoroughness from the task; default to medium:\n- **Quick**: Targeted lookups, key files only\n- **Medium**: Follow imports, read critical sections\n- **Thorough**: Trace all dependencies, check tests/types.\n</thoroughness>\n\n<procedure>\n1. Locate relevant code using tools.\n2. Read key sections. NEVER read full files unless they're tiny.\n3. Identify types/interfaces/key functions.\n4. Note dependencies between files.\n</procedure>\n\n<critical>\nYou MUST operate as read-only. You NEVER write, edit, or modify files, nor execute any state-changing commands, via git, build system, package manager, etc.\nYou MUST keep going until complete.\n</critical>\n\nYou are specializing as: **PTY Protocol 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 stale terminal characters caused by missing whitespace writes across SPT TUI and broker PTY/rc paths.\n# Constraints\nRead-only investigation. Skip formatters, linters, tests, and edits. Use CONTEXT.md terminology and relevant ADR/hazard docs. Return exact files/symbols/lines and a falsifiable root-cause account.\n# Contract\nIdentify whether picker rendering and rc broker PTY share a common diff/render/filter path, and where spaces/blank cells are lost.\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 `RcWhitespaceRca`. Currently visible peers:\n- `Main` — main (main, running)\n- `RefreshLifecycle` — Daemon Lifecycle Investigator (sub, parked)\n- `PtyControl` — PTY Transport Investigator (sub, parked)\n- `ReproHarness` — Integration Test Investigator (sub, parked)\n- `HyphenMessage` — CLI Parsing Investigator (sub, parked)\n- `ElevatedRc` — Remote Control Investigator (sub, parked)\n- `ElevatedMessaging` — Messaging Lifecycle Investigator (sub, parked)\n- `RcSplitBrain` — Registry Session Investigator (sub, parked)\n- `ElevatedHosted` — Windows Token Investigator (sub, parked)\n- `ShortformDifferential` — Delivery Differential Investigator (sub, parked)\n- `AdapterFreshSemantics` — Claude Adapter Investigator (sub, parked)\n- `PerriDurableState` — State Forensics Investigator (sub, parked)\n- `CoreFreshSemantics` — Core Lifecycle Investigator (sub, parked)\n- `RestStateRca` — Endpoint Lifecycle Investigator (sub, parked)\n- `CreateNoopRca` — CLI Semantics Investigator (sub, parked)\n- `ControlledStampRca` — Broker Control-State Investigator (sub, parked)\n- `EmphRestartRca` — Daemon Restart Investigator (sub, parked)\n- `HarnessExitRca` — Harness Lifecycle Investigator (sub, parked)\n- `V034DiffRca` — Regression Archaeologist (sub, parked)\n- `V034ReleaseRca` — Release Contract Investigator (sub, parked)\n- `LiveLogRca` — Runtime Telemetry Investigator (sub, parked)\n- `PtyPathRca` — PTY Concurrency Investigator (sub, parked)\n- `BrokerCpuRca` — Broker Concurrency Investigator (sub, parked)\n- `RedispatchCpuRca` — Release Regression Investigator (sub, parked)\n- `BrainCpuRca` — Brain Performance Investigator (sub, parked)\n- `ResidualBrainCpu` — Performance Regression Investigator (sub, parked)\n- `AdapterIdentityDiff` — Adapter Lifecycle Investigator (sub, parked)\n- `CoreSeedBoundary` — Core Security Reviewer (sub, parked)\n- `HerdrTerminalResearch` — Open-source Terminal Librarian (sub, running)\n- `PickerWhitespaceRca` — Terminal TUI Investigator (sub, running)\nIdle/parked peers are not gone: messaging them wakes (or revives) them.\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-18, 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\n`spt rc` and broker-hosted PTY rendering, including disconnect timeout and exit cleanup. Relevant crates only; non-goal: edit code.\n# Change\nTrace broker PTY bytes through viewport/diff/output to local terminal. Locate where spaces, erased cells, blank rows, ANSI clear sequences, or final teardown restoration are omitted. Identify deterministic seam.\n# Acceptance\nReport exact symbols/files/lines, byte-level reproduction, shared vs distinct causes, and fix suggestions.","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":"ae8b6832","parentId":"a37d97ab","timestamp":"2026-07-18T09:04:23.899Z","message":{"role":"user","content":[{"type":"text","text":"Complete the assignment below, thoroughly:\n\n# Target\n`spt rc` and broker-hosted PTY rendering, including disconnect timeout and exit cleanup. Relevant crates only; non-goal: edit code.\n# Change\nTrace broker PTY bytes through viewport/diff/output to local terminal. Locate where spaces, erased cells, blank rows, ANSI clear sequences, or final teardown restoration are omitted. Identify deterministic seam.\n# Acceptance\nReport exact symbols/files/lines, byte-level reproduction, shared vs distinct causes, and fix suggestions."}],"attribution":"agent","timestamp":1784365463896}}
{"type":"message","id":"ed8a2e03","parentId":"ae8b6832","timestamp":"2026-07-18T09:04:34.589Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning context and ADR investigation**"},{"type":"toolCall","id":"call_xKFwyy5ikDwVdIL6bz3aSgEg|fc_0ce5441d29b9f05d016a5b41a28dbc819bb326b9892b54e22b","name":"glob","arguments":{"i":"Mapping repository docs","path":"CONTEXT.md; docs/**/*.md; crates/**/Cargo.toml; crates/**/*.rs","hidden":true,"gitignore":true,"limit":500}},{"type":"toolCall","id":"call_jXKb66a9ICtmxEJPPbF2W5OZ|fc_0ce5441d29b9f05d016a5b41a28dcc819bb73c97c1c14de71e","name":"grep","arguments":{"i":"Locating RC PTY symbols","pattern":"\\b(rc|pty|viewport|diff|render|disconnect|timeout|cleanup|restore)\\b","path":"crates","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_NnnzrjeNM1MICXTJBAIDXjHw|fc_0ce5441d29b9f05d016a5b41a28dd0819b9117604b205e79c9","name":"grep","arguments":{"i":"Finding terminal clear logic","pattern":"ClearType|Clear\\(|Erase|ED|EL|\\x1b\\[|ANSI|alternate.?screen|LeaveAlternateScreen|Show|cursor","path":"crates","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_nWEVQwhouLUrGY9uBAX7QAvr|fc_0ce5441d29b9f05d016a5b41a28dd8819b82c350f5caf54b2e","name":"grep","arguments":{"i":"Finding whitespace filters","pattern":"trim|is_whitespace|is_ascii_whitespace|retain|filter\\(|blank|space|row","path":"crates","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14931,"output":295,"cacheRead":0,"cacheWrite":0,"totalTokens":15226,"reasoningTokens":22,"cost":{"input":0.07465500000000001,"output":0.00885,"cacheRead":0,"cacheWrite":0,"total":0.08350500000000001}},"stopReason":"toolUse","timestamp":1784365463922,"responseId":"resp_0ce5441d29b9f05d016a5b41983afc819b923338f85a741b7e","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b419d3c48819ba761ab00e894740f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0Gixm5GxuIonZ3E6mhCbFmPwJlH9YDcQbhimivADnpQ_yCWh-b6hpXncedn37yg6-RMfY7TsdixB5b9Qvziqoex5wkswIgCxD5WnoCVjjJqbKz01-fax2v00jMrcC5NPSjqx9qw5txVa7Mymmaw9trcxgfbaFe6ABCz6OHd1JvaM0vifxWAw7liTACtqknRdL4-zIfv7wZjK77WYZ1zdhF-LHnhyBpMtUJc1zdZ8kN9wp57PGgmPqfJtnmMSZ0XX9DfTCSl5lJx6dsCMeWmVM45_paG7_WYWfhQPXnvvpL77nwCGxN0qIXMnFy_XHJNX1FVTpLc00U_BaqQqDgLW2ck5wM72sDi_yEGCDJqbCxISjC75MCq5Z5YdHX0IpFljAIX7uMKoA_sPEpUCgCEduOJQZ_USzJHnF0UlyFdvv8Sn6gEbEvM87060Qku8OphvGepfum7TdLqZFwD7xBtJQK4Ke7_w7bL4iSxP8ZCql7MXINKnqBZ2t8SCT_yBit9J9RzGkQ4HVJhqq5u-DAAC-vUx_4LRoe576m-1uvV3u41aoeBUcaLOV_wbcPGqIAjlFMye98ckhk_jfybd2I8VHexnfLbDoW6jz4v2tfBQigJEu0W4KAc9Q-fkEgoRy7LmO00OztCEcc4acw4RPQdTIKrJAj_p6AEV_M_r4zeV0mpWGP5CYe4WGWKmoham5tYieoEBY_wuUtRTrTBAmKCLd7k69UoN9PDtzp4oUfsEenY6puA52KtruiB-aWwRCmgY8FevAExjeFwgfwnBJT6M-UcQd4Dm_7aqyQ94KnXmi2eHgVEv35gBwvCoit4xQkFzEdxKaUZx3pYrkD8qqwqnS2JSyzmplXiBo5Eplkt99HaE0feZj3Ds9SXzeoIeXjd-W0G-nYNeGe8mKtUT0y4I6_0BXXq9eGnrD6LiSwbx1J8LkpjevGq75WWubhIaZf88-pn3cWMB6Pr2FPlk7Res6y_hqpN8K_14eZq6kvTEdDsd3ZJ309QfoAQhdZjrzjrdlTNc1p_LwzSVv8sV2WW97GQbw==","summary":[{"type":"summary_text","text":"**Planning context and ADR investigation**"}]},{"id":"fc_0ce5441d29b9f05d016a5b41a28dbc819bb326b9892b54e22b","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping repository docs\",\"path\":\"CONTEXT.md; docs/**/*.md; crates/**/Cargo.toml; crates/**/*.rs\",\"hidden\":true,\"gitignore\":true,\"limit\":500}","call_id":"call_xKFwyy5ikDwVdIL6bz3aSgEg","name":"glob"},{"id":"fc_0ce5441d29b9f05d016a5b41a28dcc819bb73c97c1c14de71e","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating RC PTY symbols\",\"pattern\":\"\\\\b(rc|pty|viewport|diff|render|disconnect|timeout|cleanup|restore)\\\\b\",\"path\":\"crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_jXKb66a9ICtmxEJPPbF2W5OZ","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41a28dd0819b9117604b205e79c9","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding terminal clear logic\",\"pattern\":\"ClearType|Clear\\\\(|Erase|ED|EL|\\\\x1b\\\\[|ANSI|alternate.?screen|LeaveAlternateScreen|Show|cursor\",\"path\":\"crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_NnnzrjeNM1MICXTJBAIDXjHw","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41a28dd8819b82c350f5caf54b2e","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding whitespace filters\",\"pattern\":\"trim|is_whitespace|is_ascii_whitespace|retain|filter\\\\(|blank|space|row\",\"path\":\"crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_nWEVQwhouLUrGY9uBAX7QAvr","name":"grep"}]},"duration":10666.166200011969,"ttft":5308.808800011873,"contextSnapshot":{"promptTokens":14931,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xKFwyy5ikDwVdIL6bz3aSgEg|fc_0ce5441d29b9f05d016a5b41a28dbc819bb326b9892b54e22b","toolName":"glob","startedAt":"2026-07-18T09:04:34.589Z","args":{"path":"CONTEXT.md; docs/**/*.md; crates/**/Cargo.toml; crates/**/*.rs"},"intent":"Mapping repository docs"},"id":"6c750f23","parentId":"ed8a2e03","timestamp":"2026-07-18T09:04:34.589Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jXKb66a9ICtmxEJPPbF2W5OZ|fc_0ce5441d29b9f05d016a5b41a28dcc819bb73c97c1c14de71e","toolName":"grep","startedAt":"2026-07-18T09:04:34.589Z","args":{"path":"crates"},"intent":"Locating RC PTY symbols"},"id":"e132500b","parentId":"6c750f23","timestamp":"2026-07-18T09:04:34.589Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_NnnzrjeNM1MICXTJBAIDXjHw|fc_0ce5441d29b9f05d016a5b41a28dd0819b9117604b205e79c9","toolName":"grep","startedAt":"2026-07-18T09:04:34.589Z","args":{"path":"crates"},"intent":"Finding terminal clear logic"},"id":"0cab0543","parentId":"e132500b","timestamp":"2026-07-18T09:04:34.589Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_nWEVQwhouLUrGY9uBAX7QAvr|fc_0ce5441d29b9f05d016a5b41a28dd8819b82c350f5caf54b2e","toolName":"grep","startedAt":"2026-07-18T09:04:34.589Z","args":{"path":"crates"},"intent":"Finding whitespace filters"},"id":"efa87112","parentId":"0cab0543","timestamp":"2026-07-18T09:04:34.589Z"}
{"type":"message","id":"0204190e","parentId":"efa87112","timestamp":"2026-07-18T09:04:34.618Z","message":{"role":"toolResult","toolCallId":"call_xKFwyy5ikDwVdIL6bz3aSgEg|fc_0ce5441d29b9f05d016a5b41a28dbc819bb326b9892b54e22b","toolName":"glob","content":[{"type":"text","text":"CONTEXT.md\n# crates/\n## spt-daemon/\nCargo.toml\n### src/\nregistryhost.rs\nresting.rs\nlib.rs\nmsg.rs\nharnesshost.rs\nbroker.rs\nbrain.rs\nnethost.rs\nlivehost.rs\nlifecycle.rs\nattach.rs\nsync.rs\npropagate.rs\ndispatch.rs\nbrainproc.rs\nseedproofx.rs\nendpoint.rs\nprojwriter.rs\napplyhost.rs\ndocshost.rs\ndaemon.rs\nconfig.rs\nupdate.rs\nrelcache.rs\nrelease.rs\nnotif.rs\nrelay.rs\ninject.rs\nconn.rs\nautostart.rs\nstderrlog.rs\ndigest.rs\npairhost.rs\n#### pump/\nmod.rs\nhealth.rs\n#### bin/\nxlate_choreo_fixture.rs\n### tests/\nspawn_truth.rs\ndaemon_e2e.rs\nendpoint_lifecycle.rs\ndispatch.rs\nregistry_lifecycle.rs\ninject_control_wedge.rs\nattach.rs\nredispatch_stall.rs\nfalse_promote.rs\nconn_blackhole_lifecycle.rs\nbroker.rs\nbrain_decouple.rs\nmesh_recovery.rs\nredispatch.rs\ninput_ack_deadlock.rs\ndocs_server_e2e.rs\npropagate.rs\npumpdeadline.rs\npump.rs\nnet_worker_starve.rs\nbrain_resume_conn_deadlock.rs\ntwo_origin_spanning.rs\nwake_single_flight.rs\nendpoint_survival.rs\n#### fixtures/\ndispatch_fixture.rs\n## spt/\nCargo.toml\n### tests/\nrun_no_dup_session_e2e.rs\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/\nrc.rs\ncli.rs\nwansend.rs\nroster.rs\n#### api/\nstartup.rs\nreporting.rs\nauth.rs\nmod.rs\nworker.rs\ndelivery.rs\n#### picker/\ndata.rs\nmod.rs\nview.rs\nmodel.rs\n## spt-store/\nCargo.toml\n### src/\ninfo.rs\npeeraddrs.rs\nprojindex.rs\nprojderive.rs\nprojinval.rs\nlib.rs\nbranchstore.rs\ncontextstore.rs\ngitrun.rs\npsyche_custody.rs\nspool.rs\nworker_reap.rs\nworker_seq.rs\n### tests/\ncarrier_claim_int.rs\n## spt-net/\nCargo.toml\n### src/net/\nattach.rs\n#### pairing/\nntp.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\n## spt-live/src/\npulse.rs\ningest.rs\necho.rs\n# docs/\nKNOWN-HAZARDS.md\nREGISTRY-LIFECYCLE-TRIAGE.md\nRELEASE-RUNBOOK.md\nREDISPATCH-STALL-TRIAGE.md\nMESH-RECOVERY-TRIAGE.md\nREDISPATCH-TRUTH-TRIAGE.md\nSTORAGE.md\nPROJECT-INDEX-TRIAGE.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\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\n## adr/\n0041-endpoint-lifecycle-truth.md\n0040-bounded-stream-seat-lifecycle-one-way-terminal.md\n0038-broker-stream-lifecycle-truth.md\n0039-peer-route-resolution-truth.md\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\n\n[200 results limit reached. Use limit=400 for more]"}],"details":{"scopePath":"CONTEXT.md, docs/**/*.md, crates/**/Cargo.toml, crates/**/*.rs","fileCount":200,"files":["crates/spt-daemon/src/registryhost.rs","docs/adr/0041-endpoint-lifecycle-truth.md","crates/spt/tests/run_no_dup_session_e2e.rs","crates/spt/src/rc.rs","crates/spt/src/cli.rs","crates/spt-daemon/src/resting.rs","crates/spt-daemon/tests/spawn_truth.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/harnesshost.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/brain.rs","docs/adr/0040-bounded-stream-seat-lifecycle-one-way-terminal.md","crates/spt/src/api/startup.rs","crates/spt-store/src/info.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/endpoint_lifecycle.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/registry_lifecycle.rs","crates/spt-daemon/src/sync.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/propagate.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/brainproc.rs","docs/KNOWN-HAZARDS.md","docs/REGISTRY-LIFECYCLE-TRIAGE.md","docs/RELEASE-RUNBOOK.md","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt-net/src/net/attach.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/redispatch_stall.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/brain_decouple.rs","docs/REDISPATCH-STALL-TRIAGE.md","docs/adr/0038-broker-stream-lifecycle-truth.md","docs/adr/0039-peer-route-resolution-truth.md","crates/spt/src/wansend.rs","crates/spt-store/src/peeraddrs.rs","crates/spt-daemon/tests/mesh_recovery.rs","crates/spt-daemon/src/seedproofx.rs","crates/spt-daemon/src/pump/health.rs","crates/spt-daemon/src/endpoint.rs","docs/MESH-RECOVERY-TRIAGE.md","CONTEXT.md","crates/spt-daemon/tests/redispatch.rs","crates/spt-daemon/tests/input_ack_deadlock.rs","crates/spt-daemon/tests/fixtures/dispatch_fixture.rs","crates/spt-daemon/Cargo.toml","docs/REDISPATCH-TRUTH-TRIAGE.md","docs/STORAGE.md","crates/spt/tests/projindex_reader_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs","crates/spt/src/picker/data.rs","crates/spt/src/api/reporting.rs","crates/spt-store/src/projindex.rs","crates/spt-store/src/projderive.rs","crates/spt-daemon/src/projwriter.rs","docs/adr/0037-materialized-project-index.md","docs/PROJECT-INDEX-TRIAGE.md","crates/spt/tests/projindex_writer_e2e.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/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/Cargo.toml","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","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/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/F-035-CONFLICT.md","crates/spt/tests/trial_drain_drive_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/send_stamp_agent_id_e2e.rs","crates/spt/tests/endpoint_autostart_e2e.rs","crates/spt/tests/list_json_liveness_parity_e2e.rs","crates/spt/tests/listen_seed_retry_e2e.rs","crates/spt/tests/active_only_never_relay_e2e.rs","crates/spt/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/Cargo.toml","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/net_worker_starve.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/src/relay.rs","crates/spt-daemon/src/inject.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/autostart.rs","docs/PUMP-TRUTH-TRIAGE.md","docs/PUMP-W2-DISPATCH-RULING.md","docs/MUSL-RELEASE-HANDOFF.md","crates/spt-net/Cargo.toml","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","crates/spt/tests/n1_pairing.rs","crates/spt/tests/attach_wedge_e2e.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/digest.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","docs/adr/0005-totp-seeded-spake2-node-pairing.md","crates/spt-net/src/net/pairing/ntp.rs","crates/spt-daemon/src/pairhost.rs","docs/NEXT-MILESTONE-WORKERTRUTH-TRIAGE.md","docs/FLAKE-LEDGER.md","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-runtime/src/registry.rs","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","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/picker/view.rs","crates/spt/src/picker/model.rs"],"truncated":true,"resultLimitReached":200,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","meta":{"limits":{"resultLimit":{"reached":200,"suggestion":400}}}},"isError":false,"timestamp":1784365474617}}
{"type":"message","id":"72104de5","parentId":"0204190e","timestamp":"2026-07-18T09:04:34.683Z","message":{"role":"toolResult","toolCallId":"call_jXKb66a9ICtmxEJPPbF2W5OZ|fc_0ce5441d29b9f05d016a5b41a28dcc819bb73c97c1c14de71e","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|# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for\n 9|# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /\n...\n 45|serde_json = { version = \"1\", features = [\"preserve_order\"] }\n*46|# PTY output is arbitrary bytes; the forward-compat JSON envelope carries it as\n 47|# a base64 string in the payload rather than forking the codec for binary.\n 48|base64 = \"0.22\"\n 49|# Cross-platform local IPC: named pipe (Windows) / Unix-domain socket (unix)\n\n## src/\n### applyhost.rs\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 359|/// respawn lands on the SAME on-disk binary. A candidate that fails readiness\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 10|//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n*11|//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n 12|//!   journal (a replayed input record cannot double-type);\n 13|//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n 14|//!\n...\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 25|//! re-transmit output records on the wire — duplicates are dropped at the\n*26|//! render cursor, the same discipline the local `OutputLog` replay uses\n 27|//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n 28|//!\n*29|//! **Compute + files stay here**; the operator is a viewport (CONTEXT\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...\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 60|/// record to move.\n...\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 73|    Refused,\n 74|    /// The session's child exited; the `Exit` record was sent and the stream\n...\n 76|    Exited(Option<i32>),\n*77|    /// The operator closed its send side (detached); the viewport ended with\n 78|    /// the session still running.\n 79|    Detached,\n 80|    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n...\n 86|/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n*87|/// arrives — gates wake (controller-only) and PTY input.\n 88|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 89|enum ServeRole {\n 90|    /// The interactive controller (input + resize; wakes the endpoint).\n...\n 202|/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n*203|/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n 204|/// its own session table, so a remote operator — which cannot know our local\n 205|/// session id — names the endpoint and we map it here, never trusting a wire\n 206|/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n...\n 208|/// refuses cleanly, D6).\n*209|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 210|pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n 211|    brain.sessions().ok().and_then(|reply| {\n 212|        reply\n...\n 226|///   denial finishes our send side and ends the serve (`Some(Refused)`).\n*227|/// - Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): the dispatcher-resolved\n 228|///   session id wins (the owning node is authoritative for its own table);\n 229|///   the LOCAL path uses the wire `session_id` unchanged.\n 230|/// - Role-aware subscribe (REQ-RCVIEW-1): the broker resolves controller /\n...\n 234|///   the CONTROLLER keeps strict reject-gap (exactly-once resume, B2).\n*235|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 236|// [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n 237|#[allow(clippy::too_many_arguments)]\n 238|fn handle_attach_request(\n...\n 278|/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n*279|/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n*280|/// journal, and re-pumped output dedups at the operator's render cursor).\n 281|///\n 282|/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n*283|/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n 284|/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n 285|/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n 286|/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n...\n 300|// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n*301|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 302|// [impl->REQ-STREAM-OPENER-DURABLE]\n 303|#[allow(clippy::too_many_arguments)]\n 304|pub fn serve_attach(\n### autostart.rs\n 13|//! start or the other replays. Each replay is a FRESH session (shape (c)\n*14|//! restore-what-was-up was rejected): the adapter option re-resolves against\n 15|//! the live registry, the session id is freshly minted, and home rides the\n 16|//! endpoint's existing perch (durable — REQ-INST-15), so no `--subnet` is\n 17|//! recorded or needed.\n### brain.rs\n 132|    },\n*133|    /// The current PTY size of a session, pushed to a VIEWER (the letterbox feed,\n 134|    /// REQ-RCVIEW-1).\n 135|    Size {\n 136|        session_id: u64,\n...\n 215|/// cross-platform, and on Windows named pipes interprocess 2.4.2 offers NEITHER\n*216|/// portable read timeout (`set_recv_timeout` → `no_timeouts()`; `set_nonblocking`\n 217|/// → deprecated `PIPE_NOWAIT`, which corrupts mid-stream — proven by the mesh\n 218|/// E2E). So the pump carrier instead **splits** the stream: a dedicated reader\n 219|/// thread does blocking [`read_frame`] on the [`RecvHalf`] and forwards each\n...\n 221|/// reads with [`Receiver::recv_timeout`]. Blocking reads work on both OSes; the\n*222|/// channel supplies the deadline. On a timeout the pump bubbles out to a\n 223|/// supervised restart (a fresh [`Brain`] → fresh split → fresh reader thread);\n 224|/// the old reader, parked in `read_frame`, is abandoned (KH 7.6 leak watch — the\n 225|/// old conn's drop eventually errors its read and the thread exits).\n...\n 460|\n*461|    /// Spawn a session under a broker PTY; the broker auto-subscribes this\n 462|    /// connection. Reads until the `spawned` reply (consuming any output that\n 463|    /// races ahead).\n 464|    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {\n...\n 565|    /// time (the `shellchan` spool-delivery drain). For the fire-and-forward\n*566|    /// operator/rc path — which never reads the ack and can send a FLOOD on one\n 567|    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure\n 568|    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\n 569|    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n...\n 574|    /// Inject input idempotently like [`Brain::send_effect`] but request NO\n*575|    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc\n 576|    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is\n 577|    /// discarded by `serve_attach` — and it can flood N input frames on ONE\n 578|    /// brain↔broker conn without returning to read. With no ack the broker's\n...\n 606|\n*607|    /// Resize the current session's PTY.\n 608|    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {\n 609|        let sid = self.require_session()?;\n 610|        self.send(\n...\n 868|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n*869|                    // Same exactly-once discipline as PTY output, per stream:\n 870|                    // accept the contiguous next seq, drop a replay duplicate,\n 871|                    // reject a forward gap (a lost chunk must not pass silently).\n 872|                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);\n...\n 935|    /// Like [`Self::read_event_until`] but for a PEER-REPLY read (a pump leg\n*936|    /// waiting on a peer's forwarded stream response). A no-progress timeout is\n 937|    /// the peer being connect-then-silent / half-alive — an ORDINARY per-peer\n 938|    /// failure, so it is reclassified OUT of `TimedOut` and the pump's\n 939|    /// `peer_outcome` drops ONLY that peer instead of POISONING the whole round +\n...\n 991|        self.next_seq = from_seq;\n*992|        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n 993|        // --attach` case) gets a clean-screen repaint at pseudo-seq\n 994|        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n 995|        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n...\n 1026|\n*1027|    /// Arm SNAP-ABOVE at the INITIAL viewer attach — a `rc --view` VIEWER must\n 1028|    /// tolerate a **PRE-eviction** forward Output gap. Under a hard flood this\n 1029|    /// serving brain can fall behind the live ring and read a `seq` past its\n 1030|    /// cursor because the ring rolled the intervening frames OUT between reads,\n...\n 1096|    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n*1097|    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\n 1098|    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n 1099|    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n 1100|    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n...\n 1127|    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\n*1128|    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\n 1129|    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\n 1130|    /// the observation cursor is all it needs; no attach also means no controller steal\n 1131|    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\n...\n 1153|\n*1154|    /// The CONTROLLER's contiguous render cursor — the next seq still needed on\n 1155|    /// the legacy single-session (strict reject-gap) path. On a b4 drop-don't-block\n 1156|    /// output-gap this EQUALS the gap error's `want` AND the broker's authoritative\n 1157|    /// `delivered_through` (the broker advances it only on contiguous controller\n...\n 1405|    /// Mint (or reuse) the broker's single in-process loopback connection\n*1406|    /// (M12 W1.5, B1) — the transport a LOCAL `spt rc` rides, because a QUIC\n 1407|    /// self-dial is refused by iroh (\"Connecting to ourself is not supported\").\n 1408|    /// The returned [`NetDialed::remote_id_hex`] is THIS node's own id, so a\n 1409|    /// stream served over it gates same-node (access short-circuit Allow). No\n...\n 1412|    /// one-pump invariant lives at the transport leaf, not here.\n*1413|    // [impl->REQ-RC-1]\n 1414|    pub fn net_dial_loopback(&mut self) -> io::Result<NetDialed> {\n 1415|        self.send(KIND_NET_DIAL_LOOPBACK, serde_json::Value::Null)?;\n 1416|        let deadline = self.call_deadline();\n...\n 1442|    /// connection — on conn loss the broker FINs it toward the target and\n*1443|    /// terminal-retires the row. RC attach/view openers use this; inter-brain\n 1444|    /// streams stay `Durable`.\n 1445|    // [impl->REQ-STREAM-LEASE-CLASSES]\n 1446|    pub fn net_open_stream_classed(\n...\n 1655|\n*1656|    /// Deliver `bytes` to the PTY of the session hosting `endpoint`, by ENDPOINT\n 1657|    /// ID (REQ-SEND-SPT-HOSTED) — the endpoint-keyed inject a CLI `spt send` uses\n*1658|    /// to reach an spt-hosted endpoint (broker holds its PTY, no relay). The\n 1659|    /// broker resolves endpoint→session atomically and writes the bytes (a\n 1660|    /// pre-rendered `<EVENT>` envelope). Returns `(delivered, spool_deferred)`:\n 1661|    /// `delivered=true` = broker injected via translation binary; `spool_deferred=true`\n...\n 1709|    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the\n*1710|    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`\n 1711|    /// discipline on a QUIC stream, D4b).\n 1712|    // [impl->REQ-DAEMON-2]\n 1713|    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n### brainproc.rs\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...\n 184|    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n*185|    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n 186|    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n 187|    // (the live-agent adapter). A failure here is non-fatal — the brain still\n*188|    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n 189|    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n 190|    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n 191|    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n...\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...\n 228|    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n*229|    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n 230|    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n 231|    // posture. The stop flag is held for symmetry, never raised (the watchers\n 232|    // die with the brain process and the next brain re-sweeps them).\n### broker.rs\n 3|//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n*4|//! would lose continuity on if the logic restarted: the PTY master+writer and\n 5|//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n 6|//! client / listening sockets. It runs **no logic** — no routing, registry,\n*7|//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n 8|//! forward its output to the brain, inject the brain's input, report exit.\n 9|//!\n 10|//! ## Gapless handoff (B2 — the load-bearing invariant)\n...\n 28|//! Sessions are therefore **broker-owned and outlive any one connection**: a\n*29|//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n 30|//! output log persist. A session leaves the table only when its child exits\n 31|//! (reaped by the exit waiter) or is explicitly killed.\n 32|// [impl->REQ-DAEMON-2]\n...\n 92|/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n*93|/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n 94|/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n 95|/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n 96|/// owns the replay batch), so a viewer attaching to a busy session is not\n...\n 193|/// the same endpoint stops deduping and spawns itself (W4, REQ-SPAWN-COLLISION-GUARD-\n*194|/// LIVE-DUP). A racing broker spawn registers its session in PTY-spawn time (ms), so a\n 195|/// loser waits for the winner's session then no-op acks it (exactly one launch tree);\n 196|/// only a spawn that OVERRAN this ceiling (presumed dead, its claim leaked) is taken\n 197|/// over — availability over a vanishingly rare dup.\n...\n 257|/// it would be a lie — exactly when:\n*258|///   - its PTY child (the client-tree root) probes DEAD while the table record\n 259|///     survives (a wedged/missed exit-wait), OR\n 260|///   - it is an ADAPTER-labeled session (its `[session.self]` launch command\n 261|///     forks a harness client chain under the wrapper by construction), PAST\n...\n 412|// The typed idle-inject leg ate HEAD bytes nondeterministically: bytes typed into a\n*413|// PTY whose input reader is not yet attached/settled are swallowed mid-stream (the\n 414|// pre-settle race — 1854B once landed intact, ~1150B usually swallowed = a timing\n 415|// class, not a size cap). A live-SENT inject leaves NO spool copy, so a swallow is\n 416|// silent AND unrecoverable. The fix is three layers: (1) MUST settle-gate before ANY\n...\n 421|\n*422|/// The settle-gate deadline (Layer 1): how long the worker probes a PTY for a\n 423|/// positive readiness signal before typing the first payload byte. Bounded SHORT so a\n*424|/// non-echoing PTY (ConPTY does not surface a programmatic probe's echo) adds only a\n 425|/// small one-time latency — the gate is a worker-local ONE-SHOT (it protects the first\n*426|/// inject after startup, the pre-settle window; re-drives settle again). An echoing PTY\n 427|/// returns in a tick or two, well under this.\n 428|const INJECT_SETTLE_DEADLINE: Duration = Duration::from_millis(400);\n 429|/// Poll/probe cadence within the settle-gate and the echo-verify wait.\n...\n 463|\n*464|/// How many bytes of the typed payload's HEAD we look for in the PTY echo. The bug\n 465|/// is head-loss specifically, so a leading-prefix match is the discriminating probe\n 466|/// (a swallowed head means the prefix is ABSENT even though a tail may echo).\n 467|const ECHO_VERIFY_PREFIX: usize = 24;\n...\n 501|/// typed payload's HEAD echo back? Compare a bounded prefix of `sent_text` against\n*502|/// the PTY echo captured since the sequence began. A TUI re-renders typed text so the\n 503|/// leading bytes appear in output; a swallowed head means the prefix is ABSENT. An\n 504|/// empty payload (a keys-only sequence — no echoable text) is vacuously verified.\n 505|fn echo_verified(sent_text: &[u8], echoed: &[u8]) -> bool {\n...\n 619|    /// observed and only detaches if it still matches — so a slow-controller\n*620|    /// timeout can never unseat a NEWER controller that attached meanwhile. The\n 621|    /// writer snapshots its own epoch and returns the moment this value moves past\n 622|    /// it, so a SUPERSEDED writer flushes no further frames (P1c, single live\n 623|    /// writer per connection, [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\n...\n 632|    endpoint: String,\n*633|    /// Last-known PTY size — seeded at spawn, updated on a controller resize.\n 634|    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox\n*635|    /// feed); the broker holds it since the PTY exposes no size getter.\n 636|    size: (u16, u16),\n 637|    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a\n 638|    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**\n...\n 656|    delivered_through: Arc<AtomicU64>,\n*657|    /// The server-side **render grid** (#6 / REQ-BROKER-SCREEN-GRID, ADR-0031):\n 658|    /// the authoritative CURRENT SCREEN, interpreted from the same byte stream the\n 659|    /// ring records. Fed on the drain path in [`append`], AFTER the fan-out (so it\n 660|    /// never adds latency to controller/viewer delivery — hazard 7.12). A cold\n...\n 812|    ///\n*813|    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\n 814|    /// --attach` case #6 is about (attach to an already-producing TUI). A\n 815|    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n 816|    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n...\n 838|    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\n*839|    /// than the PTY floods filled its channel, the inline poll throttled the drain,\n*840|    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n 841|    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n*842|    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n 843|    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n 844|    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n 845|    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n### config.rs\n 72|/// REQ-INST-15). A replay is always a FRESH session (shape (c)\n*73|/// restore-what-was-up was rejected — this is a startup default, not a\n*74|/// session restore).\n 75|// [impl->REQ-ENDPOINT-AUTOSTART]\n 76|#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n 77|pub struct StartupEndpoint {\n...\n 81|    pub adapter: String,\n*82|    /// The project cwd the harness PTY lands in (captured at `--save`).\n 83|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 84|    pub cwd: Option<String>,\n 85|}\n### conn.rs\n 8|//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n*9|//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n*10|//! A connected consumer that stops reading (an `rc --take` whose process\n 11|//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n 12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n 13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n...\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...\n 53|//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n*54|//! cancel→disconnect window.\n 55|//!\n 56|//! No new output queue (the bounded per-sink queues + isolated writer threads\n 57|//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n...\n 159|    /// count the wedge (the W2 stall-evict tally) without conflating it with\n*160|    /// an ordinary disconnect.\n 161|    timed_out: AtomicBool,\n 162|    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\n 163|    severed: AtomicBool,\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...\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 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...\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 584|        // partially written). ──\n### crc_swap.rs\n 6|//! the moment a single install-dir binary is still running (the field bug). This\n*7|//! module replaces that with a **content-diff swap**: extract the staged archive\n 8|//! to a temp tree, replace in the install dir **only** the files whose content\n 9|//! hash differs (so identical files — and their still-running binaries — are\n 10|//! never touched), and commit via **stage-all-`.new`-then-one-tight-rename-loop**\n...\n 88|///    `<target>`.\n*89|/// 3. **Cleanup** — delete the displaced originals once the whole loop committed.\n 90|///\n 91|/// **C1 (REQ-CRC-SWAP-OLD-DISPLACE): displace, never replace.** The prior impl\n 92|/// renamed the original to a FIXED `<target>.old`. On a live box a surviving\n...\n 101|/// On ANY failure in the commit loop, the already-committed renames are **rolled\n*102|/// back** (restore each displaced original over `<target>`) and the error —\n 103|/// wrapped with the op + both paths (C1: a bare `os error 5` cost a full RCA) — is\n 104|/// returned, so the install dir is never left a half-new mix. The caller (W3d)\n 105|/// restarts the OLD resident child and reports the update failed.\n...\n 148|    // Phase 2 — tight commit loop, tracking what to roll back on failure. Each entry\n*149|    // is (target, displaced-original-path-or-None) so rollback + cleanup use the\n 150|    // ACTUAL fresh `.old[-N]` name, not a fixed one.\n 151|    let mut committed: Vec<(&Path, Option<PathBuf>)> = Vec::new();\n 152|    for swap in plan {\n...\n 255|\n*256|/// Restore each already-committed rename: move `<target>` back to `<target>.new`\n 257|/// and the displaced original back over `<target>`. Best-effort (a rollback is\n 258|/// already an error path; nothing more we can do but try).\n 259|fn rollback(\n...\n 270|\n*271|/// Remove any leftover `<target>.new` staging files (failure cleanup).\n 272|fn cleanup_new(plan: &[PlannedSwap]) {\n 273|    for swap in plan {\n 274|        let _ = std::fs::remove_file(new_path(&swap.target));\n...\n 299|    /// True iff `dir` (recursively) contains any leftover `.new` / `.old` swap\n*300|    /// litter — the never-strand cleanup invariant.\n 301|    fn has_swap_litter(dir: &Path) -> bool {\n 302|        let mut found = false;\n 303|        fn walk(dir: &Path, found: &mut bool) {\n### daemon.rs\n 3|//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds\n*4|//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the\n 5|//! canonical per-home names ([`crate::endpoint`]) and serves both for the life\n 6|//! of the process.\n 7|//!\n...\n 164|        // endpoint (closing the §B mDNS/relay-session row by construction). A\n*165|        // net failure degrades to a net-less broker — it must not cost PTY\n 166|        // hosting (mirrors the broker-bind soft-fail below).\n 167|        // [impl->REQ-DAEMON-2]\n 168|        // Identity (the durable node.key) decides node_hex independently of the\n...\n 183|        // background thread; a bind failure there must not stop the seed channel\n*184|        // (the B6 surface) — the PTY-hosting consumers arrive in B7/B8.\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...\n 253|\n*254|        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n 255|        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n 256|        // delta-stream subscribes against it. Served on a background thread like\n 257|        // the broker. The per-session feed that *populates* the hub is driven by\n...\n 347|\n*348|        // Best-effort breadcrumb: record our pid for management/cleanup. Liveness\n 349|        // is the socket ping, never this file, so a stale value is harmless.\n 350|        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());\n 351|\n...\n 705|        // otherwise dies with it (sshd HUPs the whole session on\n*706|        // disconnect; observed live in the 2026-06-06 fleet deploy, where\n 707|        // the freshly-spawned daemon vanished the moment the ssh command\n 708|        // returned). `process_group(0)` is NOT enough — the new group\n 709|        // still belongs to the dying session. Safety: this closure only\n...\n 1216|    let ran = run(&[\"/run\", \"/tn\", &task_name]);\n*1217|    // Always attempt cleanup, even if /run errored.\n 1218|    let _ = run(&[\"/delete\", \"/f\", \"/tn\", &task_name]);\n 1219|    let ran = ran?;\n 1220|    if !ran.status.success() {\n### deelevate.rs\n 216|            let mut result: *mut libc::passwd = std::ptr::null_mut();\n*217|            let rc = unsafe {\n 218|                libc::getpwnam_r(\n 219|                    cname.as_ptr(),\n 220|                    &mut pwd,\n...\n 225|            };\n*226|            if rc == libc::ERANGE && buf.len() < 1 << 20 {\n 227|                buf.resize(buf.len() * 2, 0);\n 228|                continue;\n 229|            }\n*230|            if rc != 0 || result.is_null() {\n 231|                return None;\n 232|            }\n 233|            if pwd.pw_uid == 0 {\n...\n 335|            let mut result: *mut libc::passwd = std::ptr::null_mut();\n*336|            let rc = unsafe {\n 337|                libc::getpwuid_r(\n 338|                    uid,\n 339|                    &mut pwd,\n...\n 344|            };\n*345|            if rc == libc::ERANGE && buf.len() < 1 << 20 {\n 346|                buf.resize(buf.len() * 2, 0);\n 347|                continue;\n 348|            }\n*349|            if rc != 0 || result.is_null() || pwd.pw_dir.is_null() {\n 350|                return None;\n 351|            }\n 352|            let dir = unsafe { std::ffi::CStr::from_ptr(pwd.pw_dir) };\n### digest.rs\n 6|//!\n*7|//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n 8|//! on every output chunk. The digest is now a **projection of the same normalized\n 9|//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n 10|//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n 11|//! records via [`project_endpoint_digest`], and the hub publishes the result.\n*12|//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n 13|//! read the logs.\n 14|//!\n 15|//! ## Sources (one per endpoint)\n...\n 49|/// Bound for the projection's extractor run (the `[digest]` extractor is a\n*50|/// subprocess — REQ-HAZARD-SUBPROCESS-TIMEOUT; a hang is killed, never blocks).\n 51|const DIGEST_FETCH_TIMEOUT: Duration = Duration::from_secs(30);\n 52|\n 53|/// How many recent sessions the spanning projection enumerates from the ledger\n...\n 114|\n*115|/// Length of the equal-by-value prefix shared by two turn slices — the diff base\n 116|/// for a [`DigestUpdate`] (`from`). A pure in-place/extend keeps the head intact →\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...\n 530|\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### digesthub.rs\n 107|\n*108|    /// Store a freshly projected `new` digest for `id`, diff it against the prior\n 109|    /// snapshot to compute the [`DigestUpdate`] delta, fan that out to every live\n 110|    /// subscriber (pruning any disconnected `--follow` client), and return the\n 111|    /// stored snapshot + its version. An unchanged projection is a no-op: no\n 112|    /// version bump, no fan-out. The projection itself ([`project_endpoint_digest`])\n*113|    /// runs **outside** this lock — only the diff + publish are serialized.\n 114|    // [impl->REQ-TERM-4]\n 115|    pub fn project_and_publish(&self, id: &str, new: Digest) -> (Digest, u64) {\n 116|        let mut map = self.map.lock().unwrap();\n...\n 348|\n*349|/// Render a structured digest as a human-glanceable block: one `» input` line\n 350|/// per user turn, agent output collapsed to a single line, and each tool sprint\n 351|/// as a `used: name(arg), …` line. The at-a-glance \"what is this agent doing\"\n 352|/// view (ADR-0008).\n...\n 356|\n*357|/// Render the turns carried by a streamed [`DigestUpdate`] (the delta-stream\n 358|/// presentation — same per-turn shape as a snapshot).\n 359|pub fn render_update(update: &DigestUpdate) -> String {\n 360|    render_turns(&update.turns)\n...\n 381|            match e {\n*382|                // The human render ignores seq/ts/partial (REQ-DIGEST-CURSOR) — the\n 383|                // at-a-glance view is unchanged.\n 384|                DigestEntry::Agent { text, .. } => {\n 385|                    let line = one_line(text);\n...\n 508|\n*509|    // [unit->REQ-TERM-4] the human render shows input, collapsed agent text, and a\n 510|    // single `used:` line per tool sprint.\n 511|    #[test]\n 512|    fn render_is_glanceable() {\n### dispatch.rs\n 232|/// `unknown command kind \"…\"` (broker.rs forward-compat arm), a stable N-1\n*233|/// wire fact. Everything else (transport timeout, EOF, poison) is a\n 234|/// transient — Failed → bounded requeue, never a second replay subscriber.\n 235|// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n 236|fn is_unsupported_verb(e: &io::Error) -> bool {\n...\n 742|        // to the ring peek (ADR-0038 Amendment fix 1) — the pre-Amendment\n*743|        // broad Err(_) arm caught transport timeout/EOF/poison too, and each\n 744|        // retry then manufactured a throwaway replay subscriber against a\n 745|        // possibly-wedged conn (the v0.34 stall engine's first gear).\n 746|        // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n...\n 948|            //\n*949|            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n 950|            // cannot know our local session id, so it names the ENDPOINT. When the\n 951|            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n 952|            // we actually host a live session for it (D5 session-presence gate;\n...\n 1569|    // fix 1): ONLY the old broker's literal `unknown command kind` answer may\n*1570|    // fall back to the ring peek. Transport timeout / EOF / poison — the\n 1571|    // pre-Amendment broad Err(_) classes that manufactured throwaway replay\n 1572|    // subscribers against wedged conns — must NOT pass this gate.\n 1573|    #[test]\n### drivehub.rs\n 61|/// link-break optimization — fired from `close_shell` both branches; the\n*62|/// token-stamp is the crash-proof guarantee, this is the prompt cleanup).\n 63|pub const KIND_DRIVE_CLEAR: &str = \"drive_clear\";\n 64|\n 65|/// `drive_write` request payload: the composed drive frame plus the slot key and\n...\n 163|\n*164|    /// Clear the shell's slot unconditionally (the link-break/teardown cleanup).\n 165|    // [impl->REQ-SHELL-3]\n 166|    pub fn clear(&self, owner: &str, shell_id: &str) {\n 167|        self.map.lock().unwrap().remove(&slot_key(owner, shell_id));\n### effect.rs\n 6|//! The brain restarts freely (ADR-0004): a routine self-update swaps it, a crash\n*7|//! kills it. Effects it drives cross the broker↔brain boundary — a PTY write, a\n 8|//! spool row, a registry update. A brain that crashes *around* one of those\n 9|//! effects must, on restart, neither duplicate it (a re-injected keystroke, a\n 10|//! double-written row) nor drop it (a lost delivery).\n...\n 46|//!\n*47|//! Spike #6 explicitly deferred to B5: *\"a PTY write cannot be checked-then-\n 48|//! applied atomically against a durable set the way a file append can. M3b must\n 49|//! map each boundary onto this pattern and decide where the durable applied-set\n 50|//! lives.\"* For the M3b hazard — **a brain crash, with the broker surviving** —\n...\n 75|/// Which boundary an effect crosses. Tagged in the journal so the one anchor\n*76|/// covers all three named side-effect crossings (spool / PTY / registry) — the\n*77|/// hazard requires exactly-once at *every* crossing, not just the PTY write.\n 78|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 79|pub enum EffectKind {\n*80|    /// A write into a session's PTY (the genuine broker↔brain crossing today).\n 81|    PtyWrite,\n 82|    /// A spool-row write (message delivery durability).\n 83|    Spool,\n...\n 86|    /// A side-effectful network op the broker performs for the brain (D4): a\n*87|    /// QUIC dial. Net ops cross the same broker↔brain boundary as a PTY write,\n 88|    /// so they ride the same anchor (M4-D4-PLAN: exactly-once discipline\n 89|    /// extends to net side effects).\n 90|    NetDial,\n...\n 101|        match self {\n*102|            EffectKind::PtyWrite => \"pty\",\n 103|            EffectKind::Spool => \"spool\",\n 104|            EffectKind::Registry => \"registry\",\n 105|            EffectKind::NetDial => \"net-dial\",\n...\n 113|    /// [`PtyWrite`](EffectKind::PtyWrite) is **EPHEMERAL** and is the lone\n*114|    /// exception (W1b, REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): a PTY's state is\n 115|    /// never reconstructed from keystroke replay, and a keystroke lost to a broker\n 116|    /// crash is simply retyped — so a per-keystroke fsync bought nothing but the\n 117|    /// stutter and the wedge. PtyWrite is therefore deduped **in-memory only** (the\n...\n 119|    /// durable kinds (spool / registry / net) keep their fsync'd markers.\n*120|    // [impl->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]\n 121|    pub fn is_durable(self) -> bool {\n 122|        !matches!(self, EffectKind::PtyWrite)\n 123|    }\n...\n 140|    Pump,\n*141|    Rc,\n 142|    Shell,\n 143|    Wake,\n 144|}\n...\n 157|            Minter::Pump => \"pump\",\n*158|            Minter::Rc => \"rc\",\n 159|            Minter::Shell => \"shell\",\n 160|            Minter::Wake => \"wake\",\n 161|        }\n...\n 172|            \"pump\" => Minter::Pump,\n*173|            \"rc\" => Minter::Rc,\n 174|            \"shell\" => Minter::Shell,\n 175|            \"wake\" => Minter::Wake,\n 176|            _ => return None,\n...\n 241|\n*242|/// Run a tracing-only op (ADR-0034 Decision 2: the rest family + rc attach\n 243|/// stream-open) with a ONE-shot self-heal. On the typed no-longer-held reply (a\n 244|/// broker restart dropped the conn/stream the op had journaled) re-mint a FRESH\n 245|/// op from the SAME minter and run ONCE more — a tracing op's redelivery \"needs\n...\n 292|/// The durable identity of one effect: the target class/session, the minter that\n*293|/// produced the op, and the source's durable op id (a PTY-write sequence, a\n 294|/// spool-row id, …). The dedup key — the same logical effect re-driven after a\n 295|/// crash carries the *same* key, so the applied-set recognizes the replay. The\n 296|/// `minter` dimension (ADR-0034) stops two counters that both start at 1 from\n...\n 371|    /// (`dispatch_net_stream_open` → `apply_once`) would then panic its per-conn\n*372|    /// reply thread, so attaches silently deadline (`spt rc` → \"brain IPC read\n 373|    /// deadline elapsed\") while non-journaled ops keep working. Recovering via\n 374|    /// `PoisonError::into_inner` keeps the broker serving. The protected state stays\n 375|    /// coherent on recovery: the lock is only ever held across cheap set inserts /\n...\n 391|    ///\n*392|    /// **W1b (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE):** the inner lock is RELEASED\n 393|    /// across `effect()`. Pre-W1b the lock was held across both fsyncs AND the\n 394|    /// closure, so a blocking `PtyWrite` (the operator's keystroke into a\n 395|    /// not-draining ConPTY/forkpty) parked the lock forever → the single-threaded\n*396|    /// dispatch could open no attaches (`spt rc` → \"brain IPC read deadline\"), and\n 397|    /// every keystroke paid two fsyncs serialized (the stutter). Now: reserve the\n 398|    /// key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the\n 399|    /// effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable,\n...\n 404|    /// journal lines and pays NO fsync — it is deduped in-memory only (a lost\n*405|    /// keystroke on a broker crash is retyped; PTY state is never rebuilt from\n 406|    /// keystroke replay). Durable kinds keep their fsync'd `PENDING`/`DONE` markers.\n 407|    ///\n 408|    /// The brain-crash guarantee is unchanged: the broker survives the brain, so\n...\n 414|    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*415|    // [impl->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]\n 416|    pub fn apply_once<F>(&self, key: EffectKey, kind: EffectKind, effect: F) -> io::Result<Outcome>\n 417|    where\n 418|        F: FnOnce() -> io::Result<()>,\n...\n 761|    // Both must yield exactly-once across the whole gauntlet. We cover all three\n*762|    // EffectKinds (spool / PTY / registry) by cycling the kind per op, so every\n 763|    // named boundary is value-set verified.\n 764|\n 765|    #[derive(Clone, Copy, PartialEq)]\n### endpoint.rs\n 2|//!\n*3|//! The daemon binds two local-IPC listeners — the broker (PTY hosting, B1/B2)\n 4|//! and the seed-control channel (B6) — under names derived from the active\n 5|//! `spt_home`. Deriving from `spt_home` keeps each isolated home (a test's\n 6|//! `SPT_HOME` override, a per-user install) on its own daemon, so parallel test\n...\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 66|/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the\n*67|/// running daemon writes on start — discoverable for management/cleanup. Not a\n 68|/// liveness signal (liveness is the seed-control ping); a stale pid here is\n 69|/// harmless because callers probe the socket, never this file.\n 70|pub fn daemon_pid_path() -> PathBuf {\n### harnesshost.rs\n 6|//! own perch on startup (`api listen`/`api bind` → `establish_perch`). spt-hosting\n*7|//! changes *only who owns the PTY*: the daemon spawns `[session.self]` into a\n*8|//! **broker-held PTY** ([`launch_harness_brokered_in`]). The harness still\n 9|//! self-registers its perch on bind — there is no daemon pre-register step.\n 10|//!\n*11|//! **Why broker-held.** The broker owns the PTY, so the harness survives a\n*12|//! `spt rc` detach — PTY ownership stays with the broker, no termination on\n 13|//! detach (the KNOWN-HAZARDS attach-lifecycle invariant, satisfied by\n 14|//! construction). The broker session is labeled with the endpoint id so\n*15|//! `spt rc <id>` resolves exactly this PTY.\n 16|//!\n 17|//! **Id plumbing.** The endpoint id reaches the harness two ways, both filled\n 18|//! from the SAME `{key}` catalog: argv via `{id}` substitution into the opaque\n...\n 26|//! a zero-perch `NO_PERCH`. This mirrors [`crate::shellhost::launch_shell_brokered_in`]\n*27|//! but feeds `[session.self]` (not `[shell].spawn`), uses real terminal PTY dims,\n 28|//! and mints no link token (a harness binds via `api`, not the shell link\n 29|//! handshake).\n 30|// [impl->REQ-HOST-RUN-1]\n...\n 38|\n*39|/// Default PTY geometry for a freshly brought-up harness, before an attach\n 40|/// resizes it to the operator's real terminal. A harness *is* a terminal (unlike\n 41|/// a shell's wide anti-wrap stdin conduit), so a conventional 80×24 is right.\n 42|pub const DEFAULT_HARNESS_ROWS: u16 = 24;\n...\n 212|\n*213|/// Spawn an adapter's `[session.self]` harness into a broker-held PTY (the\n 214|/// spt-hosted bringup). The broker session is labeled `endpoint_id` so\n*215|/// `spt rc <id>` resolves it; the harness self-registers its perch on bind. The\n 216|/// broker outlives this call — the session keeps running after it returns.\n 217|///\n 218|/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]\n...\n 220|///\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...\n 549|    // none yields `translation_binary = None` (the idle-inbound spool path — no raw\n*550|    // PTY inject; ADR-0022 amendment).\n 551|    #[test]\n 552|    fn prepare_threads_translation_binary_when_declared() {\n 553|        // deprecated `path` → single-element argv (bare, no install dir).\n\n### bin/\n#### xlate_choreo_fixture.rs\n 9|//!   `{\"type\":\"event\",\"envelope\":\"…\"}` — an inbound `<EVENT>`; we respond with\n*10|//!                          the CC stash/restore choreography.\n 11|//!\n 12|//! On an `event` we emit (here → spt-core, one JSON object per line on stdout,\n 13|//! flushing each so the broker's reader sees them promptly):\n...\n 34|//! (e.g. the controller's typed bytes) ever appears — only `init` / `event` /\n*35|//! content-free `input` lines do. The binary never sees PTY keystroke content.\n 36|\n 37|use std::fs::OpenOptions;\n 38|use std::io::{self, BufRead, Write};\n 39|\n*40|/// The fixed, recognizable payload token the choreography types into the PTY.\n 41|const PAYLOAD: &str = \"XLATE_OK\";\n 42|\n 43|fn main() {\n...\n 105|            //\n*106|            // RESTORE FLOW (XON / `ctrl+q` = 0x11) BEFORE the early return: the\n 107|            // `ctrl+s` (XOFF, 0x13) emitted above PAUSED terminal output. On a\n*108|            // forkpty PTY (IXON honored) that pause STRANDS every subsequent echo —\n 109|            // the deadline-flushed operator keystrokes (op_flushed), observed via the\n 110|            // child's echo, never reach the output ring → the g2 op_flushed read\n 111|            // times out (forkpty-ONLY; Windows ConPTY ignores XOFF, so g2 passed\n...\n 113|            // emits this XON before `{commit}`; an adapter that ABORTS mid-sequence\n*114|            // must restore flow just the same. We still NEVER commit, so the\n 115|            // commit-deadline FAULT path is exercised exactly as before.\n 116|            emit(&mut out, r#\"{\"key\":\"ctrl+q\"}\"#);\n 117|            continue;\n...\n 123|        // chord paused, so the payload's echo flushes deterministically on a ConPTY\n*124|        // (the int test observes byte-receipt via that echo). A real stash/restore\n 125|        // adapter restores flow at the end of its sequence just the same; this keeps\n 126|        // the `ctrl+s` chord (and its real 0x13 mapping + floor exercise) intact while\n 127|        // making the payload's arrival observable, per the test-specialist's latitude.\n\nShowing files 1-20 of 175+. Use skip=20 for the next page, or narrow paths/pattern.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":159,"fileCount":20,"files":["crates/spt-daemon/Cargo.toml","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/autostart.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/drivehub.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/harnesshost.rs"],"fileMatches":[{"path":"crates/spt-daemon/Cargo.toml","count":2},{"path":"crates/spt-daemon/src/applyhost.rs","count":1},{"path":"crates/spt-daemon/src/attach.rs","count":20},{"path":"crates/spt-daemon/src/autostart.rs","count":1},{"path":"crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","count":7},{"path":"crates/spt-daemon/src/brain.rs","count":20},{"path":"crates/spt-daemon/src/brainproc.rs","count":5},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/config.rs","count":3},{"path":"crates/spt-daemon/src/conn.rs","count":9},{"path":"crates/spt-daemon/src/crc_swap.rs","count":7},{"path":"crates/spt-daemon/src/daemon.rs","count":7},{"path":"crates/spt-daemon/src/deelevate.rs","count":6},{"path":"crates/spt-daemon/src/digest.rs","count":5},{"path":"crates/spt-daemon/src/digesthub.rs","count":6},{"path":"crates/spt-daemon/src/dispatch.rs","count":4},{"path":"crates/spt-daemon/src/drivehub.rs","count":2},{"path":"crates/spt-daemon/src/effect.rs","count":20},{"path":"crates/spt-daemon/src/endpoint.rs","count":3},{"path":"crates/spt-daemon/src/harnesshost.rs","count":11}],"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│# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for\n  9│# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /\n   │...\n 45│serde_json = { version = \"1\", features = [\"preserve_order\"] }\n*46│# PTY output is arbitrary bytes; the forward-compat JSON envelope carries it as\n 47│# a base64 string in the payload rather than forking the codec for binary.\n 48│base64 = \"0.22\"\n 49│# Cross-platform local IPC: named pipe (Windows) / Unix-domain socket (unix)\n\n## src/\n### applyhost.rs\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 359│/// respawn lands on the SAME on-disk binary. A candidate that fails readiness\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  10│//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n *11│//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n  12│//!   journal (a replayed input record cannot double-type);\n  13│//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n  14│//!\n    │...\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  25│//! re-transmit output records on the wire — duplicates are dropped at the\n *26│//! render cursor, the same discipline the local `OutputLog` replay uses\n  27│//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n  28│//!\n *29│//! **Compute + files stay here**; the operator is a viewport (CONTEXT\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    │...\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  60│/// record to move.\n    │...\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  73│    Refused,\n  74│    /// The session's child exited; the `Exit` record was sent and the stream\n    │...\n  76│    Exited(Option<i32>),\n *77│    /// The operator closed its send side (detached); the viewport ended with\n  78│    /// the session still running.\n  79│    Detached,\n  80│    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n    │...\n  86│/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]\n *87│/// arrives — gates wake (controller-only) and PTY input.\n  88│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n  89│enum ServeRole {\n  90│    /// The interactive controller (input + resize; wakes the endpoint).\n    │...\n 202│/// endpoint label in `brain.sessions()`). The server-side half of the cross-node\n*203│/// attach (#4, REQ-RC-CROSS-NODE-ATTACH): the owning node is authoritative for\n 204│/// its own session table, so a remote operator — which cannot know our local\n 205│/// session id — names the endpoint and we map it here, never trusting a wire\n 206│/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n    │...\n 208│/// refuses cleanly, D6).\n*209│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 210│pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n 211│    brain.sessions().ok().and_then(|reply| {\n 212│        reply\n    │...\n 226│///   denial finishes our send side and ends the serve (`Some(Refused)`).\n*227│/// - Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): the dispatcher-resolved\n 228│///   session id wins (the owning node is authoritative for its own table);\n 229│///   the LOCAL path uses the wire `session_id` unchanged.\n 230│/// - Role-aware subscribe (REQ-RCVIEW-1): the broker resolves controller /\n    │...\n 234│///   the CONTROLLER keeps strict reject-gap (exactly-once resume, B2).\n*235│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 236│// [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]\n 237│#[allow(clippy::too_many_arguments)]\n 238│fn handle_attach_request(\n    │...\n 278│/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n*279│/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n*280│/// journal, and re-pumped output dedups at the operator's render cursor).\n 281│///\n 282│/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n*283│/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n 284│/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n 285│/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n 286│/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n    │...\n 300│// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n*301│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 302│// [impl->REQ-STREAM-OPENER-DURABLE]\n 303│#[allow(clippy::too_many_arguments)]\n 304│pub fn serve_attach(\n### autostart.rs\n 13│//! start or the other replays. Each replay is a FRESH session (shape (c)\n*14│//! restore-what-was-up was rejected): the adapter option re-resolves against\n 15│//! the live registry, the session id is freshly minted, and home rides the\n 16│//! endpoint's existing perch (durable — REQ-INST-15), so no `--subnet` is\n 17│//! recorded or needed.\n### brain.rs\n  132│    },\n *133│    /// The current PTY size of a session, pushed to a VIEWER (the letterbox feed,\n  134│    /// REQ-RCVIEW-1).\n  135│    Size {\n  136│        session_id: u64,\n     │...\n  215│/// cross-platform, and on Windows named pipes interprocess 2.4.2 offers NEITHER\n *216│/// portable read timeout (`set_recv_timeout` → `no_timeouts()`; `set_nonblocking`\n  217│/// → deprecated `PIPE_NOWAIT`, which corrupts mid-stream — proven by the mesh\n  218│/// E2E). So the pump carrier instead **splits** the stream: a dedicated reader\n  219│/// thread does blocking [`read_frame`] on the [`RecvHalf`] and forwards each\n     │...\n  221│/// reads with [`Receiver::recv_timeout`]. Blocking reads work on both OSes; the\n *222│/// channel supplies the deadline. On a timeout the pump bubbles out to a\n  223│/// supervised restart (a fresh [`Brain`] → fresh split → fresh reader thread);\n  224│/// the old reader, parked in `read_frame`, is abandoned (KH 7.6 leak watch — the\n  225│/// old conn's drop eventually errors its read and the thread exits).\n     │...\n  460│\n *461│    /// Spawn a session under a broker PTY; the broker auto-subscribes this\n  462│    /// connection. Reads until the `spawned` reply (consuming any output that\n  463│    /// races ahead).\n  464│    pub fn spawn_session(&mut self, req: SpawnReq) -> io::Result<u64> {\n     │...\n  565│    /// time (the `shellchan` spool-delivery drain). For the fire-and-forward\n *566│    /// operator/rc path — which never reads the ack and can send a FLOOD on one\n  567│    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure\n  568│    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).\n  569│    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n     │...\n  574│    /// Inject input idempotently like [`Brain::send_effect`] but request NO\n *575│    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc\n  576│    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is\n  577│    /// discarded by `serve_attach` — and it can flood N input frames on ONE\n  578│    /// brain↔broker conn without returning to read. With no ack the broker's\n     │...\n  606│\n *607│    /// Resize the current session's PTY.\n  608│    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {\n  609│        let sid = self.require_session()?;\n  610│        self.send(\n     │...\n  868│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n *869│                    // Same exactly-once discipline as PTY output, per stream:\n  870│                    // accept the contiguous next seq, drop a replay duplicate,\n  871│                    // reject a forward gap (a lost chunk must not pass silently).\n  872│                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);\n     │...\n  935│    /// Like [`Self::read_event_until`] but for a PEER-REPLY read (a pump leg\n *936│    /// waiting on a peer's forwarded stream response). A no-progress timeout is\n  937│    /// the peer being connect-then-silent / half-alive — an ORDINARY per-peer\n  938│    /// failure, so it is reclassified OUT of `TimedOut` and the pump's\n  939│    /// `peer_outcome` drops ONLY that peer instead of POISONING the whole round +\n     │...\n  991│        self.next_seq = from_seq;\n *992│        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n  993│        // --attach` case) gets a clean-screen repaint at pseudo-seq\n  994│        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n  995│        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n     │...\n 1026│\n*1027│    /// Arm SNAP-ABOVE at the INITIAL viewer attach — a `rc --view` VIEWER must\n 1028│    /// tolerate a **PRE-eviction** forward Output gap. Under a hard flood this\n 1029│    /// serving brain can fall behind the live ring and read a `seq` past its\n 1030│    /// cursor because the ring rolled the intervening frames OUT between reads,\n     │...\n 1096│    /// is exactly such a non-draining consumer (its `run_brain` heartbeat reads this conn\n*1097│    /// only in `net_status()`/`sessions()` drain-and-discard loops; it drives no PTY\n 1098│    /// session — brainproc.rs:184 — and nothing on its side consumes the output, §3\n 1099│    /// guard), so it MUST call [`Self::resume_session_cursors`] (cursor-only, NO\n 1100│    /// subscribe) instead. This subscribing method stays for a DRAINING caller (the\n     │...\n 1127│    /// REQ-BRAIN-RESUME-NO-CONN-DEADLOCK — see the deadlock warning on\n*1128│    /// [`Self::resume_sessions`]). The daemon brain drives no broker PTY session\n 1129│    /// (brainproc.rs:184) and nothing on its side consumes session output (§3 guard), so\n 1130│    /// the observation cursor is all it needs; no attach also means no controller steal\n 1131│    /// (REQ-BRAIN-RESUME-NO-CONTROL-STEAL, a fortiori). Rides the brain-swap ⇒ seamless.\n     │...\n 1153│\n*1154│    /// The CONTROLLER's contiguous render cursor — the next seq still needed on\n 1155│    /// the legacy single-session (strict reject-gap) path. On a b4 drop-don't-block\n 1156│    /// output-gap this EQUALS the gap error's `want` AND the broker's authoritative\n 1157│    /// `delivered_through` (the broker advances it only on contiguous controller\n     │...\n 1405│    /// Mint (or reuse) the broker's single in-process loopback connection\n*1406│    /// (M12 W1.5, B1) — the transport a LOCAL `spt rc` rides, because a QUIC\n 1407│    /// self-dial is refused by iroh (\"Connecting to ourself is not supported\").\n 1408│    /// The returned [`NetDialed::remote_id_hex`] is THIS node's own id, so a\n 1409│    /// stream served over it gates same-node (access short-circuit Allow). No\n     │...\n 1412│    /// one-pump invariant lives at the transport leaf, not here.\n*1413│    // [impl->REQ-RC-1]\n 1414│    pub fn net_dial_loopback(&mut self) -> io::Result<NetDialed> {\n 1415│        self.send(KIND_NET_DIAL_LOOPBACK, serde_json::Value::Null)?;\n 1416│        let deadline = self.call_deadline();\n     │...\n 1442│    /// connection — on conn loss the broker FINs it toward the target and\n*1443│    /// terminal-retires the row. RC attach/view openers use this; inter-brain\n 1444│    /// streams stay `Durable`.\n 1445│    // [impl->REQ-STREAM-LEASE-CLASSES]\n 1446│    pub fn net_open_stream_classed(\n     │...\n 1655│\n*1656│    /// Deliver `bytes` to the PTY of the session hosting `endpoint`, by ENDPOINT\n 1657│    /// ID (REQ-SEND-SPT-HOSTED) — the endpoint-keyed inject a CLI `spt send` uses\n*1658│    /// to reach an spt-hosted endpoint (broker holds its PTY, no relay). The\n 1659│    /// broker resolves endpoint→session atomically and writes the bytes (a\n 1660│    /// pre-rendered `<EVENT>` envelope). Returns `(delivered, spool_deferred)`:\n 1661│    /// `delivered=true` = broker injected via translation binary; `spool_deferred=true`\n     │...\n 1709│    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the\n*1710│    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`\n 1711│    /// discipline on a QUIC stream, D4b).\n 1712│    // [impl->REQ-DAEMON-2]\n 1713│    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {\n### brainproc.rs\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    │...\n 184│    // brain→brain `BrainState` handoff. Today the supervised daemon brain hosts no\n*185│    // PTY sessions (net-consumers + shellwake re-derive from disk / `net-status`),\n 186│    // so this is a no-op now and forward-correct when daemon-hosted sessions land\n 187│    // (the live-agent adapter). A failure here is non-fatal — the brain still\n*188│    // serves; the next heartbeat probes broker liveness. NOTE: this is the PTY\n 189│    // message-DELIVERY axis; the orthogonal live-agent **Psyche** lifecycle (the\n 190│    // run_pulse_loop production caller) is hosted by `livehost::spawn_live_host`\n 191│    // below, keyed on the live ENDPOINT not the broker session (CONTEXT:168/177).\n    │...\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    │...\n 228│    // broker over the same socket IPC for the remote-wake dial. Its boot sweep\n*229│    // doubles as orphan cleanup, so a brain respawn re-reconciles the watcher\n 230│    // children from disk — exactly the Q5 \"re-reconciled on brain start\"\n 231│    // posture. The stop flag is held for symmetry, never raised (the watchers\n 232│    // die with the brain process and the next brain re-sweeps them).\n### broker.rs\n   3│//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n  *4│//! would lose continuity on if the logic restarted: the PTY master+writer and\n   5│//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n   6│//! client / listening sockets. It runs **no logic** — no routing, registry,\n  *7│//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n   8│//! forward its output to the brain, inject the brain's input, report exit.\n   9│//!\n  10│//! ## Gapless handoff (B2 — the load-bearing invariant)\n    │...\n  28│//! Sessions are therefore **broker-owned and outlive any one connection**: a\n *29│//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n  30│//! output log persist. A session leaves the table only when its child exits\n  31│//! (reaped by the exit waiter) or is explicitly killed.\n  32│// [impl->REQ-DAEMON-2]\n    │...\n  92│/// OVERFLOWS the viewer is evicted — the drain `try_send`s and never blocks\n *93│/// (REQ-HAZARD-VIEWER-ISOLATION). Generous enough to absorb a normal render\n  94│/// hiccup; a viewer that falls this far behind on the LIVE stream is genuinely\n  95│/// not keeping up. Replay-at-attach does NOT ride this bound (the writer thread\n  96│/// owns the replay batch), so a viewer attaching to a busy session is not\n    │...\n 193│/// the same endpoint stops deduping and spawns itself (W4, REQ-SPAWN-COLLISION-GUARD-\n*194│/// LIVE-DUP). A racing broker spawn registers its session in PTY-spawn time (ms), so a\n 195│/// loser waits for the winner's session then no-op acks it (exactly one launch tree);\n 196│/// only a spawn that OVERRAN this ceiling (presumed dead, its claim leaked) is taken\n 197│/// over — availability over a vanishingly rare dup.\n    │...\n 257│/// it would be a lie — exactly when:\n*258│///   - its PTY child (the client-tree root) probes DEAD while the table record\n 259│///     survives (a wedged/missed exit-wait), OR\n 260│///   - it is an ADAPTER-labeled session (its `[session.self]` launch command\n 261│///     forks a harness client chain under the wrapper by construction), PAST\n    │...\n 412│// The typed idle-inject leg ate HEAD bytes nondeterministically: bytes typed into a\n*413│// PTY whose input reader is not yet attached/settled are swallowed mid-stream (the\n 414│// pre-settle race — 1854B once landed intact, ~1150B usually swallowed = a timing\n 415│// class, not a size cap). A live-SENT inject leaves NO spool copy, so a swallow is\n 416│// silent AND unrecoverable. The fix is three layers: (1) MUST settle-gate before ANY\n    │...\n 421│\n*422│/// The settle-gate deadline (Layer 1): how long the worker probes a PTY for a\n 423│/// positive readiness signal before typing the first payload byte. Bounded SHORT so a\n*424│/// non-echoing PTY (ConPTY does not surface a programmatic probe's echo) adds only a\n 425│/// small one-time latency — the gate is a worker-local ONE-SHOT (it protects the first\n*426│/// inject after startup, the pre-settle window; re-drives settle again). An echoing PTY\n 427│/// returns in a tick or two, well under this.\n 428│const INJECT_SETTLE_DEADLINE: Duration = Duration::from_millis(400);\n 429│/// Poll/probe cadence within the settle-gate and the echo-verify wait.\n    │...\n 463│\n*464│/// How many bytes of the typed payload's HEAD we look for in the PTY echo. The bug\n 465│/// is head-loss specifically, so a leading-prefix match is the discriminating probe\n 466│/// (a swallowed head means the prefix is ABSENT even though a tail may echo).\n 467│const ECHO_VERIFY_PREFIX: usize = 24;\n    │...\n 501│/// typed payload's HEAD echo back? Compare a bounded prefix of `sent_text` against\n*502│/// the PTY echo captured since the sequence began. A TUI re-renders typed text so the\n 503│/// leading bytes appear in output; a swallowed head means the prefix is ABSENT. An\n 504│/// empty payload (a keys-only sequence — no echoable text) is vacuously verified.\n 505│fn echo_verified(sent_text: &[u8], echoed: &[u8]) -> bool {\n    │...\n 619│    /// observed and only detaches if it still matches — so a slow-controller\n*620│    /// timeout can never unseat a NEWER controller that attached meanwhile. The\n 621│    /// writer snapshots its own epoch and returns the moment this value moves past\n 622│    /// it, so a SUPERSEDED writer flushes no further frames (P1c, single live\n 623│    /// writer per connection, [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\n    │...\n 632│    endpoint: String,\n*633│    /// Last-known PTY size — seeded at spawn, updated on a controller resize.\n 634│    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox\n*635│    /// feed); the broker holds it since the PTY exposes no size getter.\n 636│    size: (u16, u16),\n 637│    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a\n 638│    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**\n    │...\n 656│    delivered_through: Arc<AtomicU64>,\n*657│    /// The server-side **render grid** (#6 / REQ-BROKER-SCREEN-GRID, ADR-0031):\n 658│    /// the authoritative CURRENT SCREEN, interpreted from the same byte stream the\n 659│    /// ring records. Fed on the drain path in [`append`], AFTER the fan-out (so it\n 660│    /// never adds latency to controller/viewer delivery — hazard 7.12). A cold\n    │...\n 812│    ///\n*813│    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\n 814│    /// --attach` case #6 is about (attach to an already-producing TUI). A\n 815│    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n 816│    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n    │...\n 838│    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\n*839│    /// than the PTY floods filled its channel, the inline poll throttled the drain,\n*840│    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n 841│    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n*842│    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n 843│    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n 844│    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n 845│    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n### config.rs\n 72│/// REQ-INST-15). A replay is always a FRESH session (shape (c)\n*73│/// restore-what-was-up was rejected — this is a startup default, not a\n*74│/// session restore).\n 75│// [impl->REQ-ENDPOINT-AUTOSTART]\n 76│#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n 77│pub struct StartupEndpoint {\n   │...\n 81│    pub adapter: String,\n*82│    /// The project cwd the harness PTY lands in (captured at `--save`).\n 83│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 84│    pub cwd: Option<String>,\n 85│}\n### conn.rs\n   8│//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n  *9│//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n *10│//! A connected consumer that stops reading (an `rc --take` whose process\n  11│//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n  12│//! logical stall-evict (`stall_evict_controller`) releases only the controller\n  13│//! ROLE — it never cancels the in-flight pipe write or closes the physical\n    │...\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    │...\n  53│//! data), followed by one more `CancelIoEx` covering a read re-issued in the\n *54│//! cancel→disconnect window.\n  55│//!\n  56│//! No new output queue (the bounded per-sink queues + isolated writer threads\n  57│//! already exist — the block this bounds is BELOW them), and no `PIPE_NOWAIT`\n    │...\n 159│    /// count the wedge (the W2 stall-evict tally) without conflating it with\n*160│    /// an ordinary disconnect.\n 161│    timed_out: AtomicBool,\n 162│    /// The physical sever ran (exactly once; Windows `DisconnectNamedPipe`).\n 163│    severed: AtomicBool,\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    │...\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 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    │...\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 584│        // partially written). ──\n### crc_swap.rs\n   6│//! the moment a single install-dir binary is still running (the field bug). This\n  *7│//! module replaces that with a **content-diff swap**: extract the staged archive\n   8│//! to a temp tree, replace in the install dir **only** the files whose content\n   9│//! hash differs (so identical files — and their still-running binaries — are\n  10│//! never touched), and commit via **stage-all-`.new`-then-one-tight-rename-loop**\n    │...\n  88│///    `<target>`.\n *89│/// 3. **Cleanup** — delete the displaced originals once the whole loop committed.\n  90│///\n  91│/// **C1 (REQ-CRC-SWAP-OLD-DISPLACE): displace, never replace.** The prior impl\n  92│/// renamed the original to a FIXED `<target>.old`. On a live box a surviving\n    │...\n 101│/// On ANY failure in the commit loop, the already-committed renames are **rolled\n*102│/// back** (restore each displaced original over `<target>`) and the error —\n 103│/// wrapped with the op + both paths (C1: a bare `os error 5` cost a full RCA) — is\n 104│/// returned, so the install dir is never left a half-new mix. The caller (W3d)\n 105│/// restarts the OLD resident child and reports the update failed.\n    │...\n 148│    // Phase 2 — tight commit loop, tracking what to roll back on failure. Each entry\n*149│    // is (target, displaced-original-path-or-None) so rollback + cleanup use the\n 150│    // ACTUAL fresh `.old[-N]` name, not a fixed one.\n 151│    let mut committed: Vec<(&Path, Option<PathBuf>)> = Vec::new();\n 152│    for swap in plan {\n    │...\n 255│\n*256│/// Restore each already-committed rename: move `<target>` back to `<target>.new`\n 257│/// and the displaced original back over `<target>`. Best-effort (a rollback is\n 258│/// already an error path; nothing more we can do but try).\n 259│fn rollback(\n    │...\n 270│\n*271│/// Remove any leftover `<target>.new` staging files (failure cleanup).\n 272│fn cleanup_new(plan: &[PlannedSwap]) {\n 273│    for swap in plan {\n 274│        let _ = std::fs::remove_file(new_path(&swap.target));\n    │...\n 299│    /// True iff `dir` (recursively) contains any leftover `.new` / `.old` swap\n*300│    /// litter — the never-strand cleanup invariant.\n 301│    fn has_swap_litter(dir: &Path) -> bool {\n 302│        let mut found = false;\n 303│        fn walk(dir: &Path, found: &mut bool) {\n### daemon.rs\n    3│//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds\n   *4│//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the\n    5│//! canonical per-home names ([`crate::endpoint`]) and serves both for the life\n    6│//! of the process.\n    7│//!\n     │...\n  164│        // endpoint (closing the §B mDNS/relay-session row by construction). A\n *165│        // net failure degrades to a net-less broker — it must not cost PTY\n  166│        // hosting (mirrors the broker-bind soft-fail below).\n  167│        // [impl->REQ-DAEMON-2]\n  168│        // Identity (the durable node.key) decides node_hex independently of the\n     │...\n  183│        // background thread; a bind failure there must not stop the seed channel\n *184│        // (the B6 surface) — the PTY-hosting consumers arrive in B7/B8.\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     │...\n  253│\n *254│        // The digest hub (B8) holds the live per-endpoint PTY-digest projections;\n  255│        // the digest-control channel serves `spt digest <id>` snapshot pulls +\n  256│        // delta-stream subscribes against it. Served on a background thread like\n  257│        // the broker. The per-session feed that *populates* the hub is driven by\n     │...\n  347│\n *348│        // Best-effort breadcrumb: record our pid for management/cleanup. Liveness\n  349│        // is the socket ping, never this file, so a stale value is harmless.\n  350│        let _ = std::fs::write(daemon_pid_path(), std::process::id().to_string());\n  351│\n     │...\n  705│        // otherwise dies with it (sshd HUPs the whole session on\n *706│        // disconnect; observed live in the 2026-06-06 fleet deploy, where\n  707│        // the freshly-spawned daemon vanished the moment the ssh command\n  708│        // returned). `process_group(0)` is NOT enough — the new group\n  709│        // still belongs to the dying session. Safety: this closure only\n     │...\n 1216│    let ran = run(&[\"/run\", \"/tn\", &task_name]);\n*1217│    // Always attempt cleanup, even if /run errored.\n 1218│    let _ = run(&[\"/delete\", \"/f\", \"/tn\", &task_name]);\n 1219│    let ran = ran?;\n 1220│    if !ran.status.success() {\n### deelevate.rs\n 216│            let mut result: *mut libc::passwd = std::ptr::null_mut();\n*217│            let rc = unsafe {\n 218│                libc::getpwnam_r(\n 219│                    cname.as_ptr(),\n 220│                    &mut pwd,\n    │...\n 225│            };\n*226│            if rc == libc::ERANGE && buf.len() < 1 << 20 {\n 227│                buf.resize(buf.len() * 2, 0);\n 228│                continue;\n 229│            }\n*230│            if rc != 0 || result.is_null() {\n 231│                return None;\n 232│            }\n 233│            if pwd.pw_uid == 0 {\n    │...\n 335│            let mut result: *mut libc::passwd = std::ptr::null_mut();\n*336│            let rc = unsafe {\n 337│                libc::getpwuid_r(\n 338│                    uid,\n 339│                    &mut pwd,\n    │...\n 344│            };\n*345│            if rc == libc::ERANGE && buf.len() < 1 << 20 {\n 346│                buf.resize(buf.len() * 2, 0);\n 347│                continue;\n 348│            }\n*349│            if rc != 0 || result.is_null() || pwd.pw_dir.is_null() {\n 350│                return None;\n 351│            }\n 352│            let dir = unsafe { std::ffi::CStr::from_ptr(pwd.pw_dir) };\n### digest.rs\n   6│//!\n  *7│//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n   8│//! on every output chunk. The digest is now a **projection of the same normalized\n   9│//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n  10│//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n  11│//! records via [`project_endpoint_digest`], and the hub publishes the result.\n *12│//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n  13│//! read the logs.\n  14│//!\n  15│//! ## Sources (one per endpoint)\n    │...\n  49│/// Bound for the projection's extractor run (the `[digest]` extractor is a\n *50│/// subprocess — REQ-HAZARD-SUBPROCESS-TIMEOUT; a hang is killed, never blocks).\n  51│const DIGEST_FETCH_TIMEOUT: Duration = Duration::from_secs(30);\n  52│\n  53│/// How many recent sessions the spanning projection enumerates from the ledger\n    │...\n 114│\n*115│/// Length of the equal-by-value prefix shared by two turn slices — the diff base\n 116│/// for a [`DigestUpdate`] (`from`). A pure in-place/extend keeps the head intact →\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    │...\n 530│\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### digesthub.rs\n 107│\n*108│    /// Store a freshly projected `new` digest for `id`, diff it against the prior\n 109│    /// snapshot to compute the [`DigestUpdate`] delta, fan that out to every live\n 110│    /// subscriber (pruning any disconnected `--follow` client), and return the\n 111│    /// stored snapshot + its version. An unchanged projection is a no-op: no\n 112│    /// version bump, no fan-out. The projection itself ([`project_endpoint_digest`])\n*113│    /// runs **outside** this lock — only the diff + publish are serialized.\n 114│    // [impl->REQ-TERM-4]\n 115│    pub fn project_and_publish(&self, id: &str, new: Digest) -> (Digest, u64) {\n 116│        let mut map = self.map.lock().unwrap();\n    │...\n 348│\n*349│/// Render a structured digest as a human-glanceable block: one `» input` line\n 350│/// per user turn, agent output collapsed to a single line, and each tool sprint\n 351│/// as a `used: name(arg), …` line. The at-a-glance \"what is this agent doing\"\n 352│/// view (ADR-0008).\n    │...\n 356│\n*357│/// Render the turns carried by a streamed [`DigestUpdate`] (the delta-stream\n 358│/// presentation — same per-turn shape as a snapshot).\n 359│pub fn render_update(update: &DigestUpdate) -> String {\n 360│    render_turns(&update.turns)\n    │...\n 381│            match e {\n*382│                // The human render ignores seq/ts/partial (REQ-DIGEST-CURSOR) — the\n 383│                // at-a-glance view is unchanged.\n 384│                DigestEntry::Agent { text, .. } => {\n 385│                    let line = one_line(text);\n    │...\n 508│\n*509│    // [unit->REQ-TERM-4] the human render shows input, collapsed agent text, and a\n 510│    // single `used:` line per tool sprint.\n 511│    #[test]\n 512│    fn render_is_glanceable() {\n### dispatch.rs\n  232│/// `unknown command kind \"…\"` (broker.rs forward-compat arm), a stable N-1\n *233│/// wire fact. Everything else (transport timeout, EOF, poison) is a\n  234│/// transient — Failed → bounded requeue, never a second replay subscriber.\n  235│// [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n  236│fn is_unsupported_verb(e: &io::Error) -> bool {\n     │...\n  742│        // to the ring peek (ADR-0038 Amendment fix 1) — the pre-Amendment\n *743│        // broad Err(_) arm caught transport timeout/EOF/poison too, and each\n  744│        // retry then manufactured a throwaway replay subscriber against a\n  745│        // possibly-wedged conn (the v0.34 stall engine's first gear).\n  746│        // [impl->REQ-DISPATCH-FALLBACK-CIRCUIT]\n     │...\n  948│            //\n *949│            // Cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH): a REMOTE operator\n  950│            // cannot know our local session id, so it names the ENDPOINT. When the\n  951│            // Request carries `endpoint_id`, THAT is the gate subject — but only if\n  952│            // we actually host a live session for it (D5 session-presence gate;\n     │...\n 1569│    // fix 1): ONLY the old broker's literal `unknown command kind` answer may\n*1570│    // fall back to the ring peek. Transport timeout / EOF / poison — the\n 1571│    // pre-Amendment broad Err(_) classes that manufactured throwaway replay\n 1572│    // subscribers against wedged conns — must NOT pass this gate.\n 1573│    #[test]\n### drivehub.rs\n  61│/// link-break optimization — fired from `close_shell` both branches; the\n *62│/// token-stamp is the crash-proof guarantee, this is the prompt cleanup).\n  63│pub const KIND_DRIVE_CLEAR: &str = \"drive_clear\";\n  64│\n  65│/// `drive_write` request payload: the composed drive frame plus the slot key and\n    │...\n 163│\n*164│    /// Clear the shell's slot unconditionally (the link-break/teardown cleanup).\n 165│    // [impl->REQ-SHELL-3]\n 166│    pub fn clear(&self, owner: &str, shell_id: &str) {\n 167│        self.map.lock().unwrap().remove(&slot_key(owner, shell_id));\n### effect.rs\n   6│//! The brain restarts freely (ADR-0004): a routine self-update swaps it, a crash\n  *7│//! kills it. Effects it drives cross the broker↔brain boundary — a PTY write, a\n   8│//! spool row, a registry update. A brain that crashes *around* one of those\n   9│//! effects must, on restart, neither duplicate it (a re-injected keystroke, a\n  10│//! double-written row) nor drop it (a lost delivery).\n    │...\n  46│//!\n *47│//! Spike #6 explicitly deferred to B5: *\"a PTY write cannot be checked-then-\n  48│//! applied atomically against a durable set the way a file append can. M3b must\n  49│//! map each boundary onto this pattern and decide where the durable applied-set\n  50│//! lives.\"* For the M3b hazard — **a brain crash, with the broker surviving** —\n    │...\n  75│/// Which boundary an effect crosses. Tagged in the journal so the one anchor\n *76│/// covers all three named side-effect crossings (spool / PTY / registry) — the\n *77│/// hazard requires exactly-once at *every* crossing, not just the PTY write.\n  78│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n  79│pub enum EffectKind {\n *80│    /// A write into a session's PTY (the genuine broker↔brain crossing today).\n  81│    PtyWrite,\n  82│    /// A spool-row write (message delivery durability).\n  83│    Spool,\n    │...\n  86│    /// A side-effectful network op the broker performs for the brain (D4): a\n *87│    /// QUIC dial. Net ops cross the same broker↔brain boundary as a PTY write,\n  88│    /// so they ride the same anchor (M4-D4-PLAN: exactly-once discipline\n  89│    /// extends to net side effects).\n  90│    NetDial,\n    │...\n 101│        match self {\n*102│            EffectKind::PtyWrite => \"pty\",\n 103│            EffectKind::Spool => \"spool\",\n 104│            EffectKind::Registry => \"registry\",\n 105│            EffectKind::NetDial => \"net-dial\",\n    │...\n 113│    /// [`PtyWrite`](EffectKind::PtyWrite) is **EPHEMERAL** and is the lone\n*114│    /// exception (W1b, REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): a PTY's state is\n 115│    /// never reconstructed from keystroke replay, and a keystroke lost to a broker\n 116│    /// crash is simply retyped — so a per-keystroke fsync bought nothing but the\n 117│    /// stutter and the wedge. PtyWrite is therefore deduped **in-memory only** (the\n    │...\n 119│    /// durable kinds (spool / registry / net) keep their fsync'd markers.\n*120│    // [impl->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]\n 121│    pub fn is_durable(self) -> bool {\n 122│        !matches!(self, EffectKind::PtyWrite)\n 123│    }\n    │...\n 140│    Pump,\n*141│    Rc,\n 142│    Shell,\n 143│    Wake,\n 144│}\n    │...\n 157│            Minter::Pump => \"pump\",\n*158│            Minter::Rc => \"rc\",\n 159│            Minter::Shell => \"shell\",\n 160│            Minter::Wake => \"wake\",\n 161│        }\n    │...\n 172│            \"pump\" => Minter::Pump,\n*173│            \"rc\" => Minter::Rc,\n 174│            \"shell\" => Minter::Shell,\n 175│            \"wake\" => Minter::Wake,\n 176│            _ => return None,\n    │...\n 241│\n*242│/// Run a tracing-only op (ADR-0034 Decision 2: the rest family + rc attach\n 243│/// stream-open) with a ONE-shot self-heal. On the typed no-longer-held reply (a\n 244│/// broker restart dropped the conn/stream the op had journaled) re-mint a FRESH\n 245│/// op from the SAME minter and run ONCE more — a tracing op's redelivery \"needs\n    │...\n 292│/// The durable identity of one effect: the target class/session, the minter that\n*293│/// produced the op, and the source's durable op id (a PTY-write sequence, a\n 294│/// spool-row id, …). The dedup key — the same logical effect re-driven after a\n 295│/// crash carries the *same* key, so the applied-set recognizes the replay. The\n 296│/// `minter` dimension (ADR-0034) stops two counters that both start at 1 from\n    │...\n 371│    /// (`dispatch_net_stream_open` → `apply_once`) would then panic its per-conn\n*372│    /// reply thread, so attaches silently deadline (`spt rc` → \"brain IPC read\n 373│    /// deadline elapsed\") while non-journaled ops keep working. Recovering via\n 374│    /// `PoisonError::into_inner` keeps the broker serving. The protected state stays\n 375│    /// coherent on recovery: the lock is only ever held across cheap set inserts /\n    │...\n 391│    ///\n*392│    /// **W1b (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE):** the inner lock is RELEASED\n 393│    /// across `effect()`. Pre-W1b the lock was held across both fsyncs AND the\n 394│    /// closure, so a blocking `PtyWrite` (the operator's keystroke into a\n 395│    /// not-draining ConPTY/forkpty) parked the lock forever → the single-threaded\n*396│    /// dispatch could open no attaches (`spt rc` → \"brain IPC read deadline\"), and\n 397│    /// every keystroke paid two fsyncs serialized (the stutter). Now: reserve the\n 398│    /// key (fsync `PENDING` for DURABLE kinds) under the lock, RELEASE, run the\n 399│    /// effect OFF the lock, then re-acquire to finalize (fsync `DONE` for durable,\n    │...\n 404│    /// journal lines and pays NO fsync — it is deduped in-memory only (a lost\n*405│    /// keystroke on a broker crash is retyped; PTY state is never rebuilt from\n 406│    /// keystroke replay). Durable kinds keep their fsync'd `PENDING`/`DONE` markers.\n 407│    ///\n 408│    /// The brain-crash guarantee is unchanged: the broker survives the brain, so\n    │...\n 414│    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n*415│    // [impl->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]\n 416│    pub fn apply_once<F>(&self, key: EffectKey, kind: EffectKind, effect: F) -> io::Result<Outcome>\n 417│    where\n 418│        F: FnOnce() -> io::Result<()>,\n    │...\n 761│    // Both must yield exactly-once across the whole gauntlet. We cover all three\n*762│    // EffectKinds (spool / PTY / registry) by cycling the kind per op, so every\n 763│    // named boundary is value-set verified.\n 764│\n 765│    #[derive(Clone, Copy, PartialEq)]\n### endpoint.rs\n  2│//!\n *3│//! The daemon binds two local-IPC listeners — the broker (PTY hosting, B1/B2)\n  4│//! and the seed-control channel (B6) — under names derived from the active\n  5│//! `spt_home`. Deriving from `spt_home` keeps each isolated home (a test's\n  6│//! `SPT_HOME` override, a per-user install) on its own daemon, so parallel test\n   │...\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 66│/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the\n*67│/// running daemon writes on start — discoverable for management/cleanup. Not a\n 68│/// liveness signal (liveness is the seed-control ping); a stale pid here is\n 69│/// harmless because callers probe the socket, never this file.\n 70│pub fn daemon_pid_path() -> PathBuf {\n### harnesshost.rs\n   6│//! own perch on startup (`api listen`/`api bind` → `establish_perch`). spt-hosting\n  *7│//! changes *only who owns the PTY*: the daemon spawns `[session.self]` into a\n  *8│//! **broker-held PTY** ([`launch_harness_brokered_in`]). The harness still\n   9│//! self-registers its perch on bind — there is no daemon pre-register step.\n  10│//!\n *11│//! **Why broker-held.** The broker owns the PTY, so the harness survives a\n *12│//! `spt rc` detach — PTY ownership stays with the broker, no termination on\n  13│//! detach (the KNOWN-HAZARDS attach-lifecycle invariant, satisfied by\n  14│//! construction). The broker session is labeled with the endpoint id so\n *15│//! `spt rc <id>` resolves exactly this PTY.\n  16│//!\n  17│//! **Id plumbing.** The endpoint id reaches the harness two ways, both filled\n  18│//! from the SAME `{key}` catalog: argv via `{id}` substitution into the opaque\n    │...\n  26│//! a zero-perch `NO_PERCH`. This mirrors [`crate::shellhost::launch_shell_brokered_in`]\n *27│//! but feeds `[session.self]` (not `[shell].spawn`), uses real terminal PTY dims,\n  28│//! and mints no link token (a harness binds via `api`, not the shell link\n  29│//! handshake).\n  30│// [impl->REQ-HOST-RUN-1]\n    │...\n  38│\n *39│/// Default PTY geometry for a freshly brought-up harness, before an attach\n  40│/// resizes it to the operator's real terminal. A harness *is* a terminal (unlike\n  41│/// a shell's wide anti-wrap stdin conduit), so a conventional 80×24 is right.\n  42│pub const DEFAULT_HARNESS_ROWS: u16 = 24;\n    │...\n 212│\n*213│/// Spawn an adapter's `[session.self]` harness into a broker-held PTY (the\n 214│/// spt-hosted bringup). The broker session is labeled `endpoint_id` so\n*215│/// `spt rc <id>` resolves it; the harness self-registers its perch on bind. The\n 216│/// broker outlives this call — the session keeps running after it returns.\n 217│///\n 218│/// `session_id`: the spawn-time `{session_id}` fill — a provisional [`mint_session_id`]\n    │...\n 220│///\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    │...\n 549│    // none yields `translation_binary = None` (the idle-inbound spool path — no raw\n*550│    // PTY inject; ADR-0022 amendment).\n 551│    #[test]\n 552│    fn prepare_threads_translation_binary_when_declared() {\n 553│        // deprecated `path` → single-element argv (bare, no install dir).\n\n### bin/\n#### xlate_choreo_fixture.rs\n   9│//!   `{\"type\":\"event\",\"envelope\":\"…\"}` — an inbound `<EVENT>`; we respond with\n *10│//!                          the CC stash/restore choreography.\n  11│//!\n  12│//! On an `event` we emit (here → spt-core, one JSON object per line on stdout,\n  13│//! flushing each so the broker's reader sees them promptly):\n    │...\n  34│//! (e.g. the controller's typed bytes) ever appears — only `init` / `event` /\n *35│//! content-free `input` lines do. The binary never sees PTY keystroke content.\n  36│\n  37│use std::fs::OpenOptions;\n  38│use std::io::{self, BufRead, Write};\n  39│\n *40│/// The fixed, recognizable payload token the choreography types into the PTY.\n  41│const PAYLOAD: &str = \"XLATE_OK\";\n  42│\n  43│fn main() {\n    │...\n 105│            //\n*106│            // RESTORE FLOW (XON / `ctrl+q` = 0x11) BEFORE the early return: the\n 107│            // `ctrl+s` (XOFF, 0x13) emitted above PAUSED terminal output. On a\n*108│            // forkpty PTY (IXON honored) that pause STRANDS every subsequent echo —\n 109│            // the deadline-flushed operator keystrokes (op_flushed), observed via the\n 110│            // child's echo, never reach the output ring → the g2 op_flushed read\n 111│            // times out (forkpty-ONLY; Windows ConPTY ignores XOFF, so g2 passed\n    │...\n 113│            // emits this XON before `{commit}`; an adapter that ABORTS mid-sequence\n*114│            // must restore flow just the same. We still NEVER commit, so the\n 115│            // commit-deadline FAULT path is exercised exactly as before.\n 116│            emit(&mut out, r#\"{\"key\":\"ctrl+q\"}\"#);\n 117│            continue;\n    │...\n 123│        // chord paused, so the payload's echo flushes deterministically on a ConPTY\n*124│        // (the int test observes byte-receipt via that echo). A real stash/restore\n 125│        // adapter restores flow at the end of its sequence just the same; this keeps\n 126│        // the `ctrl+s` chord (and its real 0x13 mapping + floor exercise) intact while\n 127│        // making the payload's arrival observable, per the test-specialist's latitude.","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784365474683}}
{"type":"message","id":"eb5d7588","parentId":"72104de5","timestamp":"2026-07-18T09:04:34.695Z","message":{"role":"toolResult","toolCallId":"call_nWEVQwhouLUrGY9uBAX7QAvr|fc_0ce5441d29b9f05d016a5b41a28dd8819b82c350f5caf54b2e","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n## Cargo.toml\n 2|name = \"spt-daemon\"\n*3|version.workspace = true\n 4|edition = \"2021\"\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...\n 22|# re-hosts spt-term PTYs + the spt-live lifecycle seams; these edges fix the\n*23|# crate's position in the workspace layering. Tasks B1–B8 consume them.\n 24|spt-proto = { path = \"../spt-proto\" }\n 25|spt-store = { path = \"../spt-store\" }\n 26|spt-msg = { path = \"../spt-msg\" }\n...\n 66|# the sudo-invoker drop (geteuid/setuid/setgroups/getpwuid_r). Matches the\n*67|# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half\n 68|# uses raw advapi32 externs (the daemon.rs detached_no_inherit posture).\n 69|[target.'cfg(unix)'.dependencies]\n 70|libc = \"0.2\"\n\n## src/\n### access.rs\n 10|//!\n*11|//! ## Whitelist semantics (stateful firewall, not a blanket block)\n 12|//!\n 13|//! - **Origin-node gate:** the subject is the node the inbound interaction\n 14|//!   *originates from* (where the operator is acting) — never the sender\n### applyhost.rs\n 860|            args,\n*861|            rows: 24,\n 862|            cols: 80,\n 863|            endpoint: String::new(),\n 864|            cwd: None,\n### attach.rs\n 148|///\n*149|/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\n 150|/// <= cap_chunks`). If it does not — the controller fell behind further than the\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...\n 206|/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n*207|/// (a stale registry row routed us an attach for something gone → the caller\n 208|/// refuses cleanly, D6).\n 209|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 210|pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n...\n 498|                        }\n*499|                        AttachRecord::Resize { rows, cols }\n 500|                            if attached && intent != AttachIntent::Viewer =>\n 501|                        {\n 502|                            // Controller → PTY resize (controller-EXCLUSIVE; the\n...\n 504|                            // Resize is dropped here by the intent gate too.\n*505|                            let _ = brain.resize(rows, cols);\n 506|                        }\n 507|                        // Input/Resize before Request, a viewer's input/resize,\n 508|                        // or target-direction records echoed back: noise — ignore.\n...\n 544|            // Viewer letterbox feed: the current PTY size (on attach + resize).\n*545|            BrokerEvent::Size { rows, cols, .. } if attached => {\n*546|                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n 547|                wire.net_stream_send(stream_id, &line, None, false)?;\n 548|            }\n 549|            // This controller was kicked by a different operator's `--take`.\n...\n 701|    stream_id: u64,\n*702|    rows: u16,\n 703|    cols: u16,\n 704|) -> io::Result<()> {\n*705|    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\n 706|    brain.net_stream_send(stream_id, &line, None, false)?;\n 707|    Ok(())\n 708|}\n### brain.rs\n 136|        session_id: u64,\n*137|        rows: u16,\n 138|        cols: u16,\n 139|    },\n 140|    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n...\n 539|    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery\n*540|    /// of an effect that originates from a durable source (a spool row, a\n 541|    /// sequenced injection), use [`Brain::send_effect`].\n 542|    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {\n 543|        let sid = self.require_session()?;\n...\n 607|    /// Resize the current session's PTY.\n*608|    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {\n 609|        let sid = self.require_session()?;\n 610|        self.send(\n 611|            KIND_RESIZE,\n...\n 613|                session_id: sid,\n*614|                rows,\n 615|                cols,\n 616|            })\n 617|            .expect(\"ResizeReq serializes\"),\n...\n 792|                        session_id: ev.session_id,\n*793|                        rows: ev.rows,\n 794|                        cols: ev.cols,\n 795|                    });\n 796|                }\n...\n 944|    /// contiguous, and a late reply on the dropped stream id matches nothing (the\n*945|    /// leg loops treat it as noise) — the desync the old blanket-poison feared\n 946|    /// cannot happen on the still-ordered brain-IPC channel.\n 947|    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n 948|    pub fn read_peer_reply_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n...\n 1160|    /// dropped frames exactly-once (IFF the ring still holds them, i.e.\n*1161|    /// `tail - delivered_through <= cap_chunks`; the ring trim is\n 1162|    /// `delivered_through`-blind, broker.rs). Drives the serve_attach controller\n 1163|    /// gap-resume — no mid-stream `KIND_SESSIONS` round-trip (which would re-fatal\n 1164|    /// on the same gap and discard Output).\n...\n 1427|    /// exactly-once (a replay is answered with the original stream). NOTE all\n*1428|    /// net op ids — dials, opens, sends — share ONE durable namespace; mint\n 1429|    /// them from a single durable counter. Opens `Durable` (the N-1 default);\n 1430|    /// a ConnectionBound opener uses [`Self::net_open_stream_classed`].\n 1431|    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n...\n 1442|    /// connection — on conn loss the broker FINs it toward the target and\n*1443|    /// terminal-retires the row. RC attach/view openers use this; inter-brain\n 1444|    /// streams stay `Durable`.\n 1445|    // [impl->REQ-STREAM-LEASE-CLASSES]\n 1446|    pub fn net_open_stream_classed(\n...\n 1521|\n*1522|    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A).\n 1523|    /// The dispatcher calls this when a serve loop runs to completion, so no\n 1524|    /// future dispatcher generation rediscovers and replays the stream\n 1525|    /// (KNOWN-HAZARDS 7.41). Best-effort at the caller: an older broker\n...\n 1533|    /// TERMINAL retire (ADR-0040 decision 1): eligibility retire PLUS physical\n*1534|    /// row teardown — for exchanges the caller KNOWS are over on both halves\n 1535|    /// (a one-way family's successful FIN; a requester that consumed its whole\n*1536|    /// reply). Never for a served request/reply row whose fire-and-forget\n 1537|    /// reply may still be flushing (that stays the plain retire). Best-effort\n 1538|    /// at the caller, same N-1 posture (an older broker performs a plain\n 1539|    /// eligibility retire — its serde ignores the flag).\n...\n 1755|    /// AFTER subscribing. `from_seq = u64::MAX` means the broker replays nothing\n*1756|    /// (no retained `seq >= MAX`), so a long-lived broker whose ring has evicted\n 1757|    /// old events can never hand this carrier a below-cursor gap that would falsely\n 1758|    /// trip the dead-broker restart. Contrast [`Self::net_presence_subscribe`]\n 1759|    /// (REQ-EP-4) which gap-checks from the durable cursor so a real lost liveness\n...\n 2004|\n*2005|    /// Unique namespaced socket name per test (parallel test threads never collide).\n 2006|    fn unique_name() -> String {\n 2007|        static SEQ: AtomicU32 = AtomicU32::new(0);\n 2008|        format!(\n### brainproc.rs\n 604|/// supervisor (the rollback already happened — the daemon is healthy on the old\n*605|/// binary). Reuses the one producer front door so the row rides first-fire AND\n*606|/// boundary-resurface (an undismissed rollback row re-surfaces until the\n 607|/// operator dismisses it — \"loud, not fire-once\").\n 608|///\n 609|/// **Called only with the brain DEAD.** Both rollback paths reap the candidate\n...\n 1323|    /// reason an older brain predates) defaults to `Cold` rather than failing —\n*1324|    /// the KH-2.3 forward-compat posture for the reason field's value space.\n 1325|    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n 1326|    #[test]\n 1327|    fn start_reason_arg_round_trips_and_is_lenient() {\n### broker.rs\n 122|    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n*123|    /// (a dropped brain connection removes its viewer rows); never written here.\n 124|    send: SharedSend,\n 125|    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n 126|    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n...\n 371|/// killed the binary permanently. Only a genuinely WEDGED binary misses this many\n*372|/// in a row; a commit RESETS the counter. Worst-case stall exposure stays bounded\n 373|/// at N × the deadline (the floor still releases every deadline regardless).\n 374|const INJECT_MISS_STRIKE_BUDGET: u32 = 3;\n 375|\n...\n 382|    match std::env::var(\"SPT_INJECT_MISS_STRIKE_BUDGET\") {\n*383|        Ok(n) => n.parse::<u32>().ok().filter(|b| *b >= 1).unwrap_or(INJECT_MISS_STRIKE_BUDGET),\n 384|        Err(_) => INJECT_MISS_STRIKE_BUDGET,\n 385|    }\n 386|}\n...\n 482|    match std::env::var(\"SPT_INJECT_TEXT_CHUNK\") {\n*483|        Ok(n) => n.parse::<usize>().ok().filter(|c| *c > 0).unwrap_or(INJECT_TEXT_CHUNK),\n 484|        Err(_) => INJECT_TEXT_CHUNK,\n 485|    }\n 486|}\n...\n 827|\n*828|    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\n 829|    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\n 830|    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\n 831|    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\n...\n 1042|                .iter()\n*1043|                .filter(|(s, _)| *s >= from_seq)\n 1044|                .cloned()\n 1045|                .collect(),\n 1046|        };\n...\n 1235|                    .iter()\n*1236|                    .filter(|(s, _)| *s >= from_seq)\n 1237|                    .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),\n 1238|            ),\n 1239|        }\n...\n 1301|            // dispatcher restart (equal generation) or a NEWER viewport\n*1302|            // (greater). A REPLAYED stale Request (an UNFINISHED backlog row\n*1303|            // whose viewport died raw, no FIN — the shape finished-row\n 1304|            // retirement cannot see) carries an OLDER generation than the\n 1305|            // live replacement controller and is REFUSED busy: lifecycle is\n 1306|            // still the discriminator, never origin identity. `0` on either\n...\n 1357|    /// non-blocking with eviction, like [`OutputLog::append`].\n*1358|    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {\n*1359|        self.size = (rows, cols);\n 1360|        // Keep the render grid geometry in step so a post-resize repaint reflects\n 1361|        // the new viewport (#8 resize-artifact fix, ADR-0031).\n*1362|        self.grid.resize(rows, cols);\n 1363|        if self.viewers.is_empty() {\n 1364|            return;\n 1365|        }\n*1366|        let frame = size_envelope(self.session_id, rows, cols);\n 1367|        let mut evicted: Vec<u64> = Vec::new();\n 1368|        for (vid, sink) in self.viewers.iter() {\n 1369|            if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\n...\n 1387|    /// is the current controller (a no-op if a newer brain took over — a late\n*1388|    /// disconnect can't unseat the live controller), and remove any viewer rows\n 1389|    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps\n 1390|    /// `driven_by` / `viewer_count` when a role actually changed.\n 1391|    /// Generation-validated detach (ADR-0038 Amendment fix 6): `Some(g)` with\n 1392|    /// a controller whose generation differs = a STALE worker's late release —\n*1393|    /// the controller slot is left alone (viewer rows still clean up by ptr).\n 1394|    /// `None` (N-1 / conn-cleanup) = today's ptr-identity behavior.\n 1395|    // [impl->REQ-SERVE-OWNERSHIP-GENERATION]\n 1396|    fn detach_if_gen(&mut self, sub: &SharedSend, gen: Option<u64>) {\n...\n 1400|                let before = self.viewers.len();\n*1401|                self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n 1402|                if self.viewers.len() != before {\n 1403|                    self.stamp_viewer_count();\n 1404|                }\n...\n 1419|        let before = self.viewers.len();\n*1420|        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n 1421|        // Bounded detach record (F-039 leg d) — only when a role actually changed.\n 1422|        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 1423|        if is_ctrl || self.viewers.len() != before {\n...\n 1536|/// thread; the drain thread's next `try_send` then sees `Disconnected` and\n*1537|/// evicts the row. The drain thread itself never blocks on this socket.\n 1538|fn viewer_writer(\n 1539|    send: SharedSend,\n 1540|    session_id: u64,\n...\n 1665|/// A failed write (dead operator) ends the thread; the drain's next bounded\n*1666|/// send then sees `Disconnected` and detaches the row. `Output` advances\n 1667|/// `delivered_through`; `Control` (Displaced/…) does not.\n 1668|// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n 1669|// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n...\n 1877|            .ok()\n*1878|            .filter(|d| *d > 0)\n 1879|            .unwrap_or(INPUT_QUEUE_DEPTH),\n 1880|        Err(_) => INPUT_QUEUE_DEPTH,\n 1881|    }\n### config.rs\n 43|\n*44|/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\n 45|/// authored by a node not heard within this window are evicted on the next\n 46|/// registry pump tick. Sized to ~10 default pump cadences — generous against\n 47|/// transient drops (a peer re-advertises every cadence, so one admitted feed\n*48|/// inside the window keeps its rows), short enough that a dead node identity\n 49|/// stops poisoning bare-id resolution within minutes.\n 50|pub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\n 51|\n...\n 154|    pub registry_pump_period: Duration,\n*155|    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows from a\n 156|    /// node not heard within this window are evicted on the next registry\n 157|    /// pump tick — a dead node identity must not poison bare-id resolution\n 158|    /// forever. Default [`DEFAULT_REGISTRY_EVICT_AFTER`].\n### conn.rs\n 111|/// conn can be controller + viewer + presence subscriber over its life), and\n*112|/// the label must stay BOUNDED — one truncation marker, then no further growth.\n 113|const LABEL_CAP: usize = 240;\n 114|\n 115|/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n...\n 463|    /// endpoint/session where known — e.g. `role=brain`,\n*464|    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n*465|    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n 466|    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n 467|    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 468|    pub(crate) fn describe(&self, fact: &str) {\n...\n 787|    // line), and the accreted label is BOUNDED: one truncation marker at\n*788|    // LABEL_CAP, then no further growth.\n 789|    #[test]\n 790|    fn conn_ids_are_unique_and_labels_stay_bounded() {\n 791|        let (a, _ca) = make_conn();\n...\n 800|            label.len() <= LABEL_CAP + '…'.len_utf8(),\n*801|            \"label growth is bounded at LABEL_CAP (+1 marker), got {} bytes\",\n 802|            label.len()\n 803|        );\n 804|        assert!(\n### crc_swap.rs\n 4|//! The legacy `spt adapter update` path does `remove_dir_all(install_dir)` then a\n*5|//! blanket extract — which fails on Windows with *\"Access is denied (os error 5)\"*\n 6|//! the moment a single install-dir binary is still running (the field bug). This\n 7|//! module replaces that with a **content-diff swap**: extract the staged archive\n 8|//! to a temp tree, replace in the install dir **only** the files whose content\n### daemon.rs\n 163|        // restarts); relay stance comes from daemon.json; mDNS rides the\n*164|        // endpoint (closing the §B mDNS/relay-session row by construction). A\n 165|        // net failure degrades to a net-less broker — it must not cost PTY\n 166|        // hosting (mirrors the broker-bind soft-fail below).\n 167|        // [impl->REQ-DAEMON-2]\n...\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 1025|pub fn should_null_std_handles(stderr_file_type: u32) -> bool {\n...\n 1123|/// privilege-relevant launch). The script is passed as `-EncodedCommand`\n*1124|/// (base64 of UTF-16LE) so the embedded command line — which carries spaces,\n 1125|/// double-quotes, `&`, and a single-quoted PS literal — survives Rust→CreateProcess\n 1126|/// →powershell arg parsing intact, with zero quoting hazards. Success REQUIRES\n 1127|/// both `ReturnValue == 0` AND a parsed `ProcessId`; anything else falls through\n...\n 1156|            out.status.code(),\n*1157|            String::from_utf8_lossy(&out.stderr).trim()\n 1158|        )));\n 1159|    }\n 1160|    let stdout = String::from_utf8_lossy(&out.stdout);\n*1161|    let mut nums = stdout.split_whitespace();\n 1162|    let return_value: i64 = nums\n 1163|        .next()\n 1164|        .and_then(|s| s.parse().ok())\n...\n 1212|            \"schtasks /create failed: {}\",\n*1213|            String::from_utf8_lossy(&created.stderr).trim()\n 1214|        )));\n 1215|    }\n 1216|    let ran = run(&[\"/run\", \"/tn\", &task_name]);\n...\n 1222|            \"schtasks /run failed: {}\",\n*1223|            String::from_utf8_lossy(&ran.stderr).trim()\n 1224|        )));\n 1225|    }\n 1226|    // No pid from schtasks; sentinel — readiness is the brain.ready poll.\n...\n 1274|    // NULL/absent handle (DETACHED_PROCESS rungs) all SURVIVE — nulling them is\n*1275|    // wrong (blanks a drained capture) and pointless (they never block). RED-FIRST:\n 1276|    // returning `true` unconditionally makes the non-pipe cases go red — which is\n*1277|    // exactly the mass int-test brain-log-blanking regression the file-type gate avoids.\n 1278|    #[test]\n 1279|    fn null_std_handles_only_on_a_pipe_stderr() {\n 1280|        const FILE_TYPE_UNKNOWN: u32 = 0x0000;\n...\n 1299|    // and caps at NET_RETRY_CAP — so a long boot-race wait settles to a steady\n*1300|    // 30s poll, never an ever-growing or zero-length spin.\n 1301|    #[test]\n 1302|    fn net_retry_backoff_doubles_then_caps() {\n 1303|        assert_eq!(net_retry_backoff(NET_RETRY_FIRST), Duration::from_secs(2));\n...\n 1309|    // [unit->REQ-HAZARD-DETACHED-PIPE-INHERIT] the no-inherit spawn's command\n*1310|    // line obeys CommandLineToArgvW quoting: bare words pass through; spaces\n 1311|    // quote; embedded quotes and the backslash-before-quote cases escape per\n 1312|    // the rules (a wrong quoting would mis-tokenize the shell spawn template).\n 1313|    #[cfg(windows)]\n...\n 1607|                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n*1608|                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609|                        if p != 0 {\n 1610|                            found = Some(p);\n 1611|                            break;\n### deelevate.rs\n 9|//! sudo flips `$HOME` — can mint a *different node identity* in root's\n*10|//! universe (the key-flip behind the 4.10 ghost rows).\n 11|//!\n 12|//! The invariant: **the daemon always runs unelevated in the invoking\n 13|//! user's universe, regardless of which command spawns it.** Two\n...\n 31|//! The OS probes mirror `spt`'s REQ-PAIR-6 `elevation.rs` (the CLI gate);\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...\n 108|        let key = k.to_ascii_uppercase();\n*109|        entries.retain(|e| name_of(e) != key);\n 110|        entries.push(format!(\"{k}={v}\").encode_utf16().collect());\n 111|    }\n 112|    let mut out: Vec<u16> = Vec::new();\n...\n 198|        let name = std::fs::read_to_string(path).ok()?;\n*199|        user_by_name(name.trim())\n 200|    }\n 201|\n 202|    /// Resolve a username to a drop target via `getpwnam_r`. Root refuses —\n...\n 319|        std::io::stdin().read_line(&mut s).ok()?;\n*320|        let s = s.trim().to_string();\n 321|        (!s.is_empty()).then_some(s)\n 322|    }\n 323|\n*324|    /// The uid's home dir via `getpwuid_r` (reentrant; buffer grown on\n 325|    /// ERANGE). `None` on any failure — the caller then leaves `HOME`\n 326|    /// alone rather than guessing.\n 327|    fn home_of(uid: u32) -> Option<PathBuf> {\n...\n 873|            got.iter()\n*874|                .filter(|e| e.to_ascii_uppercase().starts_with(\"SPT_HOME=\"))\n 875|                .count(),\n 876|            1\n 877|        );\n### digest.rs\n 139|        .map(|i| i.session_id.clone())\n*140|        .filter(|s| !s.is_empty());\n 141|\n 142|    // Resolve the adapter's manifest (with profile) by the perch's recorded\n 143|    // adapter. The record's `source_dir` is the adapter's install dir — where a\n...\n 189|        // Collapse cross-generation replay dupes on the RAW span, before the\n*190|        // two-origin merge + window fold, so a checkpoint/resume row surfaces once\n 191|        // (under its live generation) not twice. [impl->REQ-DIGEST-GENERATION-SUPERSEDE]\n 192|        supersede_cross_generation(spanned)\n 193|    } else {\n...\n 227|    // (session_id, ts, boundary-kind, ordinal) oldest→newest. A fresh/pre-ledger\n*228|    // perch has no row → the current bound session anchors at ordinal 0.\n 229|    let sessions: Vec<(String, Option<String>, String, u64)> = if ledger.is_empty() {\n 230|        current_session\n 231|            .map(|s| vec![(s.to_string(), None, \"boot\".to_string(), 0u64)])\n...\n 322|/// A checkpoint/resume replays an ancestor generation's transcript verbatim, so a\n*323|/// replayed row is BYTE-identical on exactly these fields; NUL-joined so distinct\n 324|/// field boundaries never collide.\n 325|// [impl->REQ-DIGEST-GENERATION-SUPERSEDE]\n 326|fn logical_key(r: &spt_term::DigestRecord) -> String {\n...\n 343|/// harness REPLAY the prior generation's transcript into the new session file, so\n*344|/// one logical row surfaces under two `seq`-generations — `(gen_old, localseq)`\n 345|/// and `(gen_new, localseq)` — and exact-seq consumer dedup can't collapse them.\n 346|/// Keep only the NEWEST-ordinal occurrence of each logical record (the surviving\n 347|/// `seq` is the live generation, so snapshot + `--follow` agree). Cross-generation\n...\n 349|/// replay itself) and MUST survive. Boundary dividers left adjacent to a\n*350|/// now-emptied ancestor are trimmed so no orphaned divider remains.\n 351|// [impl->REQ-DIGEST-GENERATION-SUPERSEDE]\n 352|fn supersede_cross_generation(items: Vec<spt_term::TimelineItem>) -> Vec<spt_term::TimelineItem> {\n 353|    use spt_term::TimelineItem;\n...\n 381|\n*382|    // Pass 3: keep a Boundary only BETWEEN two sessions that each retain ≥1 activity\n*383|    // row. A leading boundary (ancestor fully superseded → emptied), a trailing\n*384|    // boundary, and consecutive boundaries (a middle session emptied) are trimmed;\n 385|    // consecutive → the LAST wins (the divider INTO the surviving newer session).\n 386|    let mut out: Vec<TimelineItem> = Vec::with_capacity(kept.len());\n 387|    let mut seen_activity = false;\n...\n 424|    for (ln, raw) in s.lines().enumerate() {\n*425|        if raw.trim().is_empty() || spt_term::parse_context_record(raw).is_some() {\n*426|            continue; // blank, or an spt context entry (merged separately)\n 427|        }\n 428|        match spt_term::record_to_tagged_result(raw) {\n 429|            Ok(rec) => {\n...\n 535|        let a = vec![turn(\"a\"), turn(\"b\")];\n*536|        let grown = vec![turn(\"a\"), turn(\"b\"), turn(\"c\")];\n*537|        assert_eq!(common_prefix_len(&a, &grown), 2, \"shared head of two\");\n 538|        let slid = vec![turn(\"b\"), turn(\"c\"), turn(\"d\")];\n 539|        assert_eq!(common_prefix_len(&a, &slid), 0, \"front eviction → 0\");\n 540|    }\n...\n 682|    // [unit->REQ-DIGEST-GENERATION-SUPERSEDE] a checkpoint/resume replays gen A's\n*683|    // rows into gen B at the same localseq → the span carries each logical row\n 684|    // twice (two ordinals). Supersede keeps ONE, under B's (newer) generation, and\n 685|    // the surviving seq is B's.\n 686|    #[test]\n...\n 688|        let items = vec![\n*689|            sup_act(\"shared row\", \"2026-06-13T21:00:00Z\", 0, 0), // gen A\n 690|            sup_bound(),\n*691|            sup_act(\"shared row\", \"2026-06-13T21:00:00Z\", 1, 0), // gen B replay (same ts/text/localseq)\n*692|            sup_act(\"B tail\", \"2026-06-13T21:00:05Z\", 1, 1),     // B's own new row\n 693|        ];\n 694|        let out = supersede_cross_generation(items);\n 695|        assert_eq!(\n 696|            texts(&out),\n*697|            vec![\"shared row\", \"B tail\"],\n*698|            \"the replayed row appears ONCE, plus B's tail (no phantom double)\"\n 699|        );\n*700|        // The surviving \"shared row\" carries B's generation (ordinal 1), so a\n 701|        // consumer's exact-seq cursor tracks the live gen.\n 702|        let seq = out.iter().find_map(|it| match it {\n### digesthub.rs\n 130|        st.snapshot = new.clone();\n*131|        st.subscribers.retain(|tx| tx.send(update.clone()).is_ok());\n 132|        (new, st.version)\n 133|    }\n 134|\n...\n 419|\n*420|/// Collapse internal whitespace/newlines to a single spaced line for glanceable\n 421|/// rendering (the digest is a summary, not a transcript).\n 422|fn one_line(s: &str) -> String {\n*423|    s.split_whitespace().collect::<Vec<_>>().join(\" \")\n 424|}\n 425|\n 426|#[cfg(test)]\n...\n 462|\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### dispatch.rs\n 14|//! The stream table holds locally-opened outbound streams too. The dispatcher\n*15|//! claims only rows with `initiated_locally == false` — serving our own\n 16|//! requester stream would deadlock the requester against ourselves. The flag\n 17|//! is broker-recorded at registration (`open_stream` vs the accept loop),\n 18|//! never inferred.\n...\n 33|//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\n*34|//! | `kind=row` | notif feed ([`crate::notifsync::apply_notif_feed`]) |\n 35|//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\n 36|//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\n 37|//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n...\n 95|    Update,\n*96|    /// A notif replication feed (`NotifRecord::Row`).\n 97|    Notif,\n 98|    /// A WAN message feed (untagged `WanMessage` lines).\n 99|    WanMsg,\n...\n 134|        Some(\"status_query\") => StreamFamily::Update,\n*135|        Some(\"row\") => StreamFamily::Notif,\n 136|        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\n 137|        Some(_) => StreamFamily::Unknown,\n 138|        None => {\n...\n 147|                // A node-level label feed (REQ-SUBNET-3): an endpoint-less peer\n*148|                // sends label records first (no instance rows), so the feed's\n 149|                // first line is a NodeLabelUpdate. Same serve arm, same gate.\n 150|                StreamFamily::Registry\n 151|            } else {\n...\n 295|\n*296|/// PURE claim decision: may this row be (re)claimed now? `Some(prior\n 297|/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n 298|/// off). The unit-tested seam for the no-hot-loop ruling.\n 299|// [impl->REQ-DISPATCH-CLAIM-RETRY]\n...\n 308|/// Strike budget for a deadline-poisoned ONE-WAY replay (ADR-0040 decision 4,\n*309|/// REQ-REGISTRY-REPLAY-BOUNDED): at budget the row retires TERMINAL — safe by\n 310|/// family semantics (a feed is an idempotent snapshot advertisement; the next\n 311|/// pump round re-advertises, so worst-case loss is one ~30s round, already\n 312|/// the family's freshness unit). Request/reply families are NEVER on this\n...\n 318|/// (ADR-0040 decision 4). Registry only: a feed is idempotent and\n*319|/// re-advertised every round. Notif/WanMsg feeds carry durable rows and\n 320|/// replies respectively — dropping one loses real state, so they keep\n 321|/// request/reply semantics here even though their transport is feed-shaped.\n 322|// [impl->REQ-REGISTRY-REPLAY-BOUNDED]\n...\n 327|/// PURE outcome transition: what a finished worker's claim becomes, plus\n*328|/// whether the row must ALSO retire terminal (the one-way budget path).\n 329|/// Served and Unknown (unclassifiable) are TERMINAL — a terminal\n 330|/// classification never hot-loops. A failure requeues with backoff until the\n 331|/// bounded budget is spent, then goes terminal.\n...\n 341|/// Registry poisons). Its Transient budget exhaustion retires terminal too —\n*342|/// every terminal-from-failure one-way row leaves the table for good; the\n 343|/// next advertisement round is the recovery. Defense-in-depth behind\n 344|/// REQ-ONEWAY-STREAM-TERMINAL (which removes the population at source).\n 345|// [impl->REQ-DISPATCH-CLAIM-RETRY]\n...\n 397|/// PURE lifecycle+family discriminator (gate round 1, ADR-0038 Decision 1's\n*398|/// \"excluded by lifecycle state\" half): is a `finished` row TERMINAL for\n 399|/// redispatch — retire unserved, never replay?\n 400|///\n 401|/// `finished` alone is NOT the answer, and a bare `s.finished` claim filter\n...\n 417|// [impl->REQ-HAZARD-REDISPATCH-CONTROL-STEAL]\n*418|fn finished_row_is_terminal(family: StreamFamily, finished: bool) -> bool {\n 419|    finished && family == StreamFamily::Attach\n 420|}\n 421|\n...\n 455|/// unlike the bare pre-classification valve above) — proving strikes\n*456|/// accumulate per stream and the budget retires the row TERMINAL while\n 457|/// fresh feeds keep serving. Counted latch, armed once per process.\n 458|fn take_injected_registry_seatblocked() -> bool {\n 459|    use std::sync::atomic::AtomicUsize;\n...\n 520|    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n*521|    // with a large backlog) claims at most this many rows per poll — the\n 522|    // rest wait for the next tick, batching the cold enumeration instead of\n*523|    // spawning one thread per row. No claim lock is held across any I/O:\n 524|    // enumeration runs lock-free, the claim map is touched in short scopes.\n 525|    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n 526|    let active_workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n...\n 537|        for s in reply.streams {\n*538|            // Claim only peer-initiated rows with bytes (a QUIC stream is\n 539|            // invisible until its first write anyway — gotcha #6 — but the\n*540|            // next_seq guard also skips a row whose pump hasn't appended yet).\n*541|            // Terminal rows never re-enter; a released (Retry) claim re-enters\n 542|            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n 543|            if s.initiated_locally || s.next_seq == 0 {\n 544|                continue;\n...\n 578|                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n*579|                    // this stream's exchange is over — take its row out of\n 580|                    // every FUTURE dispatcher generation's enumeration (the\n 581|                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n 582|                    // an older broker without the verb answers error (KH-2.3)\n*583|                    // and the row retires when the broker cycles.\n 584|                    if let Ok(mut retire_brain) = connect(&name) {\n 585|                        let _ = retire_brain.net_stream_retire(stream_id);\n 586|                    }\n### docshost.rs\n 39|    if let Some(v) = env_value {\n*40|        if let Ok(p) = v.trim().parse::<u16>() {\n 41|            if p != 0 {\n 42|                return p;\n 43|            }\n...\n 86|    }\n*87|    let trimmed = decoded.trim_start_matches('/');\n*88|    let rel = if trimmed.is_empty() {\n 89|        \"index.html\"\n 90|    } else {\n*91|        trimmed\n 92|    };\n 93|    let candidate = PathBuf::from(rel);\n 94|    for comp in candidate.components() {\n...\n 237|            // Serve one connection to completion; an error here is one\n*238|            // browser's problem, never the server's.\n 239|            let _ = hyper::server::conn::http1::Builder::new()\n 240|                .serve_connection(io, service)\n 241|                .await;\n### effect.rs\n 7|//! kills it. Effects it drives cross the broker↔brain boundary — a PTY write, a\n*8|//! spool row, a registry update. A brain that crashes *around* one of those\n 9|//! effects must, on restart, neither duplicate it (a re-injected keystroke, a\n*10|//! double-written row) nor drop it (a lost delivery).\n 11|//!\n 12|//! ## The journal lines (`PENDING` / `DONE`) — and why a `PENDING`-no-`DONE` is normal\n 13|//!\n...\n 21|//! shape of an effect that did not complete: for a `net-send`, an **unconfirmed\n*22|//! write to an offline/unreachable peer**, retained as the re-drive recovery\n 23|//! marker (it re-drives on the next brain restart / reachability). Two reader\n 24|//! caveats: the log is **append-only**, so a failed-then-re-driven effect keeps\n 25|//! its stale `PENDING` line AND appends a fresh `PENDING`+`DONE` — a raw\n...\n 81|    PtyWrite,\n*82|    /// A spool-row write (message delivery durability).\n 83|    Spool,\n 84|    /// A registry / `info.json` mutation.\n 85|    Registry,\n...\n 90|    NetDial,\n*91|    /// A QUIC stream open or stream write (D4b) — same anchor, same namespace\n 92|    /// as [`EffectKind::NetDial`] (one durable op-id counter across net ops).\n 93|    NetSend,\n 94|}\n...\n 133|/// journal line is self-describing (see [`Minter::as_tag`]).\n*134|// [impl->REQ-OPID-MINTER-NAMESPACE]\n 135|#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\n 136|#[serde(rename_all = \"snake_case\")]\n 137|pub enum Minter {\n...\n 150|    /// SOURCE (the seq's counter origin), never the subsystem the call lives in —\n*151|    /// a `wake` op minted from `now_ms()` is not the `shell` spool-row counter\n 152|    /// (doyle ruling: subsystem-based tagging is the named anti-pattern).\n 153|    pub fn as_tag(self) -> &'static str {\n 154|        match self {\n...\n 293|/// produced the op, and the source's durable op id (a PTY-write sequence, a\n*294|/// spool-row id, …). The dedup key — the same logical effect re-driven after a\n 295|/// crash carries the *same* key, so the applied-set recognizes the replay. The\n 296|/// `minter` dimension (ADR-0034) stops two counters that both start at 1 from\n*297|/// colliding in the one journal namespace.\n 298|#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n 299|pub struct EffectKey {\n 300|    pub class: u64,\n...\n 523|        let line = line?;\n*524|        let mut parts = line.split_whitespace();\n 525|        match parts.next() {\n 526|            Some(\"PENDING\") => {\n 527|                // OLD: `PENDING {class} {op} {kind}` (3 tokens after marker) → Legacy.\n...\n 545|            }\n*546|            _ => {} // blank / partial / unknown line — skip\n 547|        }\n 548|    }\n 549|    Ok((applied, pending))\n...\n 646|            .apply_once(key, EffectKind::PtyWrite, || {\n*647|                *runs.borrow_mut() += 1;\n 648|                Ok(())\n 649|            })\n 650|            .unwrap();\n...\n 652|            .apply_once(key, EffectKind::PtyWrite, || {\n*653|                *runs.borrow_mut() += 1;\n 654|                Ok(())\n 655|            })\n 656|            .unwrap();\n...\n 663|        );\n*664|        assert_eq!(*runs.borrow(), 1, \"the effect ran exactly once\");\n 665|        assert!(j.is_applied(key));\n 666|    }\n 667|\n...\n 744|            .apply_once(key, EffectKind::Registry, || {\n*745|                *runs.borrow_mut() += 1;\n 746|                Ok(())\n 747|            })\n 748|            .unwrap();\n 749|        assert_eq!(o, Outcome::Deduped);\n*750|        assert_eq!(*runs.borrow(), 0, \"no re-run after recovery\");\n 751|    }\n 752|\n 753|    // ---- The Spike #6 gauntlet, on the brain-crash model -------------------\n...\n 794|                .apply_once(key, kind_for(op), || {\n*795|                    effects.borrow_mut().push(op);\n 796|                    Ok(())\n 797|                })\n 798|                .expect(\"apply_once\");\n...\n 911|            j.apply_once(pty_key, EffectKind::PtyWrite, || {\n*912|                *runs.borrow_mut() += 1;\n 913|                Ok(())\n 914|            })\n 915|            .unwrap(),\n...\n 920|            j.apply_once(net_key, EffectKind::NetSend, || {\n*921|                *runs.borrow_mut() += 1;\n 922|                Ok(())\n 923|            })\n 924|            .unwrap(),\n...\n 926|        );\n*927|        assert_eq!(*runs.borrow(), 0, \"neither replay re-ran its effect\");\n 928|\n 929|        // FILE CONTENT proxy for fsync: durable NetSend is on disk; ephemeral\n 930|        // PtyWrite is not. Keys are written as \"sid op\" tokens on the journal line.\n### endpoint.rs\n 99|    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;\n*100|    serde_json::from_str::<u64>(s.trim()).ok()\n 101|}\n 102|\n 103|/// Read the pump's reachability health snapshot (`identity/pump-health.json`,\n### grants.rs\n 22|//! (M5-PLAN scope decision 2): the ids exist so the store/gate shape is final,\n*23|//! but [`decide`] refuses them **unconditionally — even if a grant row\n*24|//! exists** (defense in depth: a stray row authored today must not become a\n 25|//! live authorization the day the capability lands; landing it is an explicit\n 26|//! code change that removes the id from [`RESERVED_CAPABILITIES`]).\n 27|// [impl->REQ-CONSENT-1]\n...\n 42|/// `require_approval = \"remembered\"` allow-always answer writes a\n*43|/// `spawn-shell × agent × (node, shell-adapter)` row (qualifier = adapter).\n 44|pub const CAP_SPAWN_SHELL: &str = \"spawn-shell\";\n 45|/// A shell suspending its linked owner directly (`api owner-shutdown`) —\n 46|/// pre-consented via the shell manifest's `can_shutdown` flag (D1c authoring\n...\n 50|/// Capability ids whose *gate shape* ships in M5 but whose behavior is\n*51|/// deferred — refused unconditionally regardless of grant rows.\n 52|pub const RESERVED_CAPABILITIES: &[&str] = &[CAP_REMOTE_EXEC, CAP_INSTANTIATE_ANYWHERE];\n 53|\n 54|/// The gate's verdict for one `(capability, agent, node, qualifier)` ask.\n...\n 56|pub enum GrantDecision {\n*57|    /// A grant row covers the ask — proceed.\n 58|    Allowed,\n 59|    /// No grant — route an interactive escalation to the carried\n 60|    /// most-recently-active session (allow-once / allow-always / deny, D1b).\n...\n 71|/// The pure grant gate (REQ-CONSENT-1). Order: reserved-refusal first (defense\n*72|/// in depth — a stray row for a deferred capability is inert), then the store\n 73|/// lookup (exact-tuple, default-deny), else escalation with the\n 74|/// caller-resolved `target`. No I/O, no clock.\n 75|// [impl->REQ-CONSENT-1]\n...\n 137|    /// (`consent-needed cap=… agent=… node=… [qual=…]`) + a human hint.\n*138|    /// Fields are SPT ids / pubkey hex / adapter names — whitespace-free by\n*139|    /// charset (REQ-HAZARD-ID-CHARSET), so space-separated `key=value` holds.\n 140|    // [impl->REQ-CONSENT-2]\n 141|    pub fn body(&self) -> String {\n 142|        let qual = self\n...\n 167|        let line = body.lines().next()?;\n*168|        let mut parts = line.split_whitespace();\n 169|        if parts.next()? != ESCALATION_PREFIX {\n 170|            return None;\n 171|        }\n...\n 209|    now_ms: u64,\n*210|) -> Result<(spt_store::notif::NotifRow, crate::notif::FirstFireOutcome), String> {\n 211|    crate::notif::produce_and_first_fire(\n 212|        store,\n 213|        node_hex,\n...\n 232|\n*233|/// The qualifier narrowing a flag-authored `instantiate-anywhere` row to the\n 234|/// shell-wake path: `shell_wake_spawn_anywhere` pre-consents shells to\n 235|/// fresh-spawn **their owner, to wake it** — not general remote\n 236|/// instantiation (CONTEXT §Shell sleep/wake).\n...\n 262|/// Author the `shell_wake_spawn_anywhere` pre-consent for `owner`: an\n*263|/// `instantiate-anywhere` row qualified to the shell-wake path. **Flag shape\n 264|/// only in M5** — the capability is reserved, so [`decide`] still refuses it\n*265|/// unconditionally (M5-PLAN scope decision 2); the row becomes live the day\n 266|/// instantiate-anywhere lands and leaves [`RESERVED_CAPABILITIES`]. This\n*267|/// deliberate authoring path is the one sanctioned way such a row exists\n 268|/// (the CLI refuses casual reserved-id authoring).\n 269|// [impl->REQ-CONSENT-2]\n 270|pub fn author_wake_spawn_anywhere_grant(store: &mut GrantStore, owner: &str, node: &str) {\n...\n 278|    AllowOnce,\n*279|    /// Proceed and write the grant row — future asks pass the gate silently.\n 280|    AllowAlways,\n*281|    /// Refuse — no row, the action does not proceed.\n 282|    Deny,\n 283|}\n 284|\n...\n 289|    ProceedOnce,\n*290|    /// Proceed; the grant row is in `store` (caller persists it).\n 291|    ProceedGranted,\n 292|    /// Do not proceed; nothing persisted.\n 293|    Denied,\n...\n 334|    // [unit->REQ-CONSENT-1] default-deny: empty store escalates (with the\n*335|    // resolved target attached); a grant row flips the same ask to Allowed;\n 336|    // revocation flips it back.\n 337|    #[test]\n 338|    fn ungranted_escalates_granted_allows() {\n...\n 391|    // [unit->REQ-CONSENT-1] reserved capabilities refuse unconditionally —\n*392|    // even with a grant row present (defense in depth) and even with a live\n 393|    // escalation target available: neither grants nor escalation are\n 394|    // consulted for a deferred capability.\n 395|    #[test]\n...\n 447|        );\n*448|        assert!(s.rows.is_empty(), \"once never persists\");\n 449|\n 450|        assert_eq!(\n 451|            apply_escalation_answer(&mut s, &a, EscalationAnswer::Deny),\n\n### bin/\n#### xlate_choreo_fixture.rs\n 54|        let Ok(line) = line else { break }; // stdin closed (EOF) → exit (no-zombie).\n*55|        let trimmed = line.trim();\n*56|        if trimmed.is_empty() {\n 57|            continue;\n 58|        }\n 59|\n...\n 63|            if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {\n*64|                let _ = writeln!(f, \"{trimmed}\");\n 65|            }\n 66|        }\n 67|\n...\n 69|        // content-free `input` ping are noted (logged above) and ignored.\n*70|        let v: serde_json::Value = match serde_json::from_str(trimmed) {\n 71|            Ok(v) => v,\n 72|            Err(_) => continue,\n 73|        };\n...\n 142|/// would false-pass). ~12 lines of ~70 cols ≈ 850 B — larger than the field's ~322 B\n*143|/// truncation and many settle-chunks wide, yet it fits inside a 24-row screen grid so a\n 144|/// cold-attach repaint (ADR-0031 = current screen) still carries the HEAD (a taller\n 145|/// payload scrolls HEADSTART off the visible grid).\n 146|fn big_multiline_payload() -> String {\n\nShowing files 1-20 of 259+. 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":173,"fileCount":20,"files":["crates/spt-daemon/Cargo.toml","crates/spt-daemon/src/access.rs","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/conn.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/digesthub.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/docshost.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/grants.rs"],"fileMatches":[{"path":"crates/spt-daemon/Cargo.toml","count":3},{"path":"crates/spt-daemon/src/access.rs","count":1},{"path":"crates/spt-daemon/src/applyhost.rs","count":1},{"path":"crates/spt-daemon/src/attach.rs","count":8},{"path":"crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","count":5},{"path":"crates/spt-daemon/src/brain.rs","count":14},{"path":"crates/spt-daemon/src/brainproc.rs","count":3},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/config.rs","count":3},{"path":"crates/spt-daemon/src/conn.rs","count":5},{"path":"crates/spt-daemon/src/crc_swap.rs","count":1},{"path":"crates/spt-daemon/src/daemon.rs","count":12},{"path":"crates/spt-daemon/src/deelevate.rs","count":7},{"path":"crates/spt-daemon/src/digest.rs","count":20},{"path":"crates/spt-daemon/src/digesthub.rs","count":4},{"path":"crates/spt-daemon/src/dispatch.rs","count":20},{"path":"crates/spt-daemon/src/docshost.rs","count":5},{"path":"crates/spt-daemon/src/effect.rs","count":20},{"path":"crates/spt-daemon/src/endpoint.rs","count":1},{"path":"crates/spt-daemon/src/grants.rs","count":20}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/\n## Cargo.toml\n  2│name = \"spt-daemon\"\n *3│version.workspace = true\n  4│edition = \"2021\"\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   │...\n 22│# re-hosts spt-term PTYs + the spt-live lifecycle seams; these edges fix the\n*23│# crate's position in the workspace layering. Tasks B1–B8 consume them.\n 24│spt-proto = { path = \"../spt-proto\" }\n 25│spt-store = { path = \"../spt-store\" }\n 26│spt-msg = { path = \"../spt-msg\" }\n   │...\n 66│# the sudo-invoker drop (geteuid/setuid/setgroups/getpwuid_r). Matches the\n*67│# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half\n 68│# uses raw advapi32 externs (the daemon.rs detached_no_inherit posture).\n 69│[target.'cfg(unix)'.dependencies]\n 70│libc = \"0.2\"\n\n## src/\n### access.rs\n 10│//!\n*11│//! ## Whitelist semantics (stateful firewall, not a blanket block)\n 12│//!\n 13│//! - **Origin-node gate:** the subject is the node the inbound interaction\n 14│//!   *originates from* (where the operator is acting) — never the sender\n### applyhost.rs\n 860│            args,\n*861│            rows: 24,\n 862│            cols: 80,\n 863│            endpoint: String::new(),\n 864│            cwd: None,\n### attach.rs\n 148│///\n*149│/// Exactly-once holds IFF the ring still retains the floor (`tail - delivered_through\n 150│/// <= cap_chunks`). If it does not — the controller fell behind further than the\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    │...\n 206│/// `session_id` it could not have. `None` ⇒ no live session under that endpoint\n*207│/// (a stale registry row routed us an attach for something gone → the caller\n 208│/// refuses cleanly, D6).\n 209│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n 210│pub(crate) fn resolve_local_session(brain: &mut Brain, endpoint: &str) -> Option<u64> {\n    │...\n 498│                        }\n*499│                        AttachRecord::Resize { rows, cols }\n 500│                            if attached && intent != AttachIntent::Viewer =>\n 501│                        {\n 502│                            // Controller → PTY resize (controller-EXCLUSIVE; the\n    │...\n 504│                            // Resize is dropped here by the intent gate too.\n*505│                            let _ = brain.resize(rows, cols);\n 506│                        }\n 507│                        // Input/Resize before Request, a viewer's input/resize,\n 508│                        // or target-direction records echoed back: noise — ignore.\n    │...\n 544│            // Viewer letterbox feed: the current PTY size (on attach + resize).\n*545│            BrokerEvent::Size { rows, cols, .. } if attached => {\n*546│                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n 547│                wire.net_stream_send(stream_id, &line, None, false)?;\n 548│            }\n 549│            // This controller was kicked by a different operator's `--take`.\n    │...\n 701│    stream_id: u64,\n*702│    rows: u16,\n 703│    cols: u16,\n 704│) -> io::Result<()> {\n*705│    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\n 706│    brain.net_stream_send(stream_id, &line, None, false)?;\n 707│    Ok(())\n 708│}\n### brain.rs\n  136│        session_id: u64,\n *137│        rows: u16,\n  138│        cols: u16,\n  139│    },\n  140│    /// This controller was DISPLACED by a different operator's `--take`/`kick`\n     │...\n  539│    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery\n *540│    /// of an effect that originates from a durable source (a spool row, a\n  541│    /// sequenced injection), use [`Brain::send_effect`].\n  542│    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {\n  543│        let sid = self.require_session()?;\n     │...\n  607│    /// Resize the current session's PTY.\n *608│    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {\n  609│        let sid = self.require_session()?;\n  610│        self.send(\n  611│            KIND_RESIZE,\n     │...\n  613│                session_id: sid,\n *614│                rows,\n  615│                cols,\n  616│            })\n  617│            .expect(\"ResizeReq serializes\"),\n     │...\n  792│                        session_id: ev.session_id,\n *793│                        rows: ev.rows,\n  794│                        cols: ev.cols,\n  795│                    });\n  796│                }\n     │...\n  944│    /// contiguous, and a late reply on the dropped stream id matches nothing (the\n *945│    /// leg loops treat it as noise) — the desync the old blanket-poison feared\n  946│    /// cannot happen on the still-ordered brain-IPC channel.\n  947│    // [impl->REQ-PUMP-DIAL-FASTFAIL]\n  948│    pub fn read_peer_reply_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {\n     │...\n 1160│    /// dropped frames exactly-once (IFF the ring still holds them, i.e.\n*1161│    /// `tail - delivered_through <= cap_chunks`; the ring trim is\n 1162│    /// `delivered_through`-blind, broker.rs). Drives the serve_attach controller\n 1163│    /// gap-resume — no mid-stream `KIND_SESSIONS` round-trip (which would re-fatal\n 1164│    /// on the same gap and discard Output).\n     │...\n 1427│    /// exactly-once (a replay is answered with the original stream). NOTE all\n*1428│    /// net op ids — dials, opens, sends — share ONE durable namespace; mint\n 1429│    /// them from a single durable counter. Opens `Durable` (the N-1 default);\n 1430│    /// a ConnectionBound opener uses [`Self::net_open_stream_classed`].\n 1431│    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n     │...\n 1442│    /// connection — on conn loss the broker FINs it toward the target and\n*1443│    /// terminal-retires the row. RC attach/view openers use this; inter-brain\n 1444│    /// streams stay `Durable`.\n 1445│    // [impl->REQ-STREAM-LEASE-CLASSES]\n 1446│    pub fn net_open_stream_classed(\n     │...\n 1521│\n*1522│    /// Retire a completed stream row from redispatch eligibility (ADR-0038 A).\n 1523│    /// The dispatcher calls this when a serve loop runs to completion, so no\n 1524│    /// future dispatcher generation rediscovers and replays the stream\n 1525│    /// (KNOWN-HAZARDS 7.41). Best-effort at the caller: an older broker\n     │...\n 1533│    /// TERMINAL retire (ADR-0040 decision 1): eligibility retire PLUS physical\n*1534│    /// row teardown — for exchanges the caller KNOWS are over on both halves\n 1535│    /// (a one-way family's successful FIN; a requester that consumed its whole\n*1536│    /// reply). Never for a served request/reply row whose fire-and-forget\n 1537│    /// reply may still be flushing (that stays the plain retire). Best-effort\n 1538│    /// at the caller, same N-1 posture (an older broker performs a plain\n 1539│    /// eligibility retire — its serde ignores the flag).\n     │...\n 1755│    /// AFTER subscribing. `from_seq = u64::MAX` means the broker replays nothing\n*1756│    /// (no retained `seq >= MAX`), so a long-lived broker whose ring has evicted\n 1757│    /// old events can never hand this carrier a below-cursor gap that would falsely\n 1758│    /// trip the dead-broker restart. Contrast [`Self::net_presence_subscribe`]\n 1759│    /// (REQ-EP-4) which gap-checks from the durable cursor so a real lost liveness\n     │...\n 2004│\n*2005│    /// Unique namespaced socket name per test (parallel test threads never collide).\n 2006│    fn unique_name() -> String {\n 2007│        static SEQ: AtomicU32 = AtomicU32::new(0);\n 2008│        format!(\n### brainproc.rs\n  604│/// supervisor (the rollback already happened — the daemon is healthy on the old\n *605│/// binary). Reuses the one producer front door so the row rides first-fire AND\n *606│/// boundary-resurface (an undismissed rollback row re-surfaces until the\n  607│/// operator dismisses it — \"loud, not fire-once\").\n  608│///\n  609│/// **Called only with the brain DEAD.** Both rollback paths reap the candidate\n     │...\n 1323│    /// reason an older brain predates) defaults to `Cold` rather than failing —\n*1324│    /// the KH-2.3 forward-compat posture for the reason field's value space.\n 1325│    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]\n 1326│    #[test]\n 1327│    fn start_reason_arg_round_trips_and_is_lenient() {\n### broker.rs\n  122│    /// The viewer's IPC send half — kept ONLY for identity matching on detach\n *123│    /// (a dropped brain connection removes its viewer rows); never written here.\n  124│    send: SharedSend,\n  125│    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),\n  126│    /// distinguishing an eviction from a normal teardown (session end / conn drop).\n     │...\n  371│/// killed the binary permanently. Only a genuinely WEDGED binary misses this many\n *372│/// in a row; a commit RESETS the counter. Worst-case stall exposure stays bounded\n  373│/// at N × the deadline (the floor still releases every deadline regardless).\n  374│const INJECT_MISS_STRIKE_BUDGET: u32 = 3;\n  375│\n     │...\n  382│    match std::env::var(\"SPT_INJECT_MISS_STRIKE_BUDGET\") {\n *383│        Ok(n) => n.parse::<u32>().ok().filter(|b| *b >= 1).unwrap_or(INJECT_MISS_STRIKE_BUDGET),\n  384│        Err(_) => INJECT_MISS_STRIKE_BUDGET,\n  385│    }\n  386│}\n     │...\n  482│    match std::env::var(\"SPT_INJECT_TEXT_CHUNK\") {\n *483│        Ok(n) => n.parse::<usize>().ok().filter(|c| *c > 0).unwrap_or(INJECT_TEXT_CHUNK),\n  484│        Err(_) => INJECT_TEXT_CHUNK,\n  485│    }\n  486│}\n     │...\n  827│\n *828│    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\n  829│    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\n  830│    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\n  831│    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\n     │...\n 1042│                .iter()\n*1043│                .filter(|(s, _)| *s >= from_seq)\n 1044│                .cloned()\n 1045│                .collect(),\n 1046│        };\n     │...\n 1235│                    .iter()\n*1236│                    .filter(|(s, _)| *s >= from_seq)\n 1237│                    .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),\n 1238│            ),\n 1239│        }\n     │...\n 1301│            // dispatcher restart (equal generation) or a NEWER viewport\n*1302│            // (greater). A REPLAYED stale Request (an UNFINISHED backlog row\n*1303│            // whose viewport died raw, no FIN — the shape finished-row\n 1304│            // retirement cannot see) carries an OLDER generation than the\n 1305│            // live replacement controller and is REFUSED busy: lifecycle is\n 1306│            // still the discriminator, never origin identity. `0` on either\n     │...\n 1357│    /// non-blocking with eviction, like [`OutputLog::append`].\n*1358│    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {\n*1359│        self.size = (rows, cols);\n 1360│        // Keep the render grid geometry in step so a post-resize repaint reflects\n 1361│        // the new viewport (#8 resize-artifact fix, ADR-0031).\n*1362│        self.grid.resize(rows, cols);\n 1363│        if self.viewers.is_empty() {\n 1364│            return;\n 1365│        }\n*1366│        let frame = size_envelope(self.session_id, rows, cols);\n 1367│        let mut evicted: Vec<u64> = Vec::new();\n 1368│        for (vid, sink) in self.viewers.iter() {\n 1369│            if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\n     │...\n 1387│    /// is the current controller (a no-op if a newer brain took over — a late\n*1388│    /// disconnect can't unseat the live controller), and remove any viewer rows\n 1389│    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps\n 1390│    /// `driven_by` / `viewer_count` when a role actually changed.\n 1391│    /// Generation-validated detach (ADR-0038 Amendment fix 6): `Some(g)` with\n 1392│    /// a controller whose generation differs = a STALE worker's late release —\n*1393│    /// the controller slot is left alone (viewer rows still clean up by ptr).\n 1394│    /// `None` (N-1 / conn-cleanup) = today's ptr-identity behavior.\n 1395│    // [impl->REQ-SERVE-OWNERSHIP-GENERATION]\n 1396│    fn detach_if_gen(&mut self, sub: &SharedSend, gen: Option<u64>) {\n     │...\n 1400│                let before = self.viewers.len();\n*1401│                self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n 1402│                if self.viewers.len() != before {\n 1403│                    self.stamp_viewer_count();\n 1404│                }\n     │...\n 1419│        let before = self.viewers.len();\n*1420│        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n 1421│        // Bounded detach record (F-039 leg d) — only when a role actually changed.\n 1422│        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 1423│        if is_ctrl || self.viewers.len() != before {\n     │...\n 1536│/// thread; the drain thread's next `try_send` then sees `Disconnected` and\n*1537│/// evicts the row. The drain thread itself never blocks on this socket.\n 1538│fn viewer_writer(\n 1539│    send: SharedSend,\n 1540│    session_id: u64,\n     │...\n 1665│/// A failed write (dead operator) ends the thread; the drain's next bounded\n*1666│/// send then sees `Disconnected` and detaches the row. `Output` advances\n 1667│/// `delivered_through`; `Control` (Displaced/…) does not.\n 1668│// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n 1669│// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n     │...\n 1877│            .ok()\n*1878│            .filter(|d| *d > 0)\n 1879│            .unwrap_or(INPUT_QUEUE_DEPTH),\n 1880│        Err(_) => INPUT_QUEUE_DEPTH,\n 1881│    }\n### config.rs\n  43│\n *44│/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\n  45│/// authored by a node not heard within this window are evicted on the next\n  46│/// registry pump tick. Sized to ~10 default pump cadences — generous against\n  47│/// transient drops (a peer re-advertises every cadence, so one admitted feed\n *48│/// inside the window keeps its rows), short enough that a dead node identity\n  49│/// stops poisoning bare-id resolution within minutes.\n  50│pub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\n  51│\n    │...\n 154│    pub registry_pump_period: Duration,\n*155│    /// Silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows from a\n 156│    /// node not heard within this window are evicted on the next registry\n 157│    /// pump tick — a dead node identity must not poison bare-id resolution\n 158│    /// forever. Default [`DEFAULT_REGISTRY_EVICT_AFTER`].\n### conn.rs\n 111│/// conn can be controller + viewer + presence subscriber over its life), and\n*112│/// the label must stay BOUNDED — one truncation marker, then no further growth.\n 113│const LABEL_CAP: usize = 240;\n 114│\n 115│/// One bounded write's outcome (beyond `Err` = poisoned/timed-out/failed).\n    │...\n 463│    /// endpoint/session where known — e.g. `role=brain`,\n*464│    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n*465│    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n 466│    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n 467│    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n 468│    pub(crate) fn describe(&self, fact: &str) {\n    │...\n 787│    // line), and the accreted label is BOUNDED: one truncation marker at\n*788│    // LABEL_CAP, then no further growth.\n 789│    #[test]\n 790│    fn conn_ids_are_unique_and_labels_stay_bounded() {\n 791│        let (a, _ca) = make_conn();\n    │...\n 800│            label.len() <= LABEL_CAP + '…'.len_utf8(),\n*801│            \"label growth is bounded at LABEL_CAP (+1 marker), got {} bytes\",\n 802│            label.len()\n 803│        );\n 804│        assert!(\n### crc_swap.rs\n 4│//! The legacy `spt adapter update` path does `remove_dir_all(install_dir)` then a\n*5│//! blanket extract — which fails on Windows with *\"Access is denied (os error 5)\"*\n 6│//! the moment a single install-dir binary is still running (the field bug). This\n 7│//! module replaces that with a **content-diff swap**: extract the staged archive\n 8│//! to a temp tree, replace in the install dir **only** the files whose content\n### daemon.rs\n  163│        // restarts); relay stance comes from daemon.json; mDNS rides the\n *164│        // endpoint (closing the §B mDNS/relay-session row by construction). A\n  165│        // net failure degrades to a net-less broker — it must not cost PTY\n  166│        // hosting (mirrors the broker-bind soft-fail below).\n  167│        // [impl->REQ-DAEMON-2]\n     │...\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 1025│pub fn should_null_std_handles(stderr_file_type: u32) -> bool {\n     │...\n 1123│/// privilege-relevant launch). The script is passed as `-EncodedCommand`\n*1124│/// (base64 of UTF-16LE) so the embedded command line — which carries spaces,\n 1125│/// double-quotes, `&`, and a single-quoted PS literal — survives Rust→CreateProcess\n 1126│/// →powershell arg parsing intact, with zero quoting hazards. Success REQUIRES\n 1127│/// both `ReturnValue == 0` AND a parsed `ProcessId`; anything else falls through\n     │...\n 1156│            out.status.code(),\n*1157│            String::from_utf8_lossy(&out.stderr).trim()\n 1158│        )));\n 1159│    }\n 1160│    let stdout = String::from_utf8_lossy(&out.stdout);\n*1161│    let mut nums = stdout.split_whitespace();\n 1162│    let return_value: i64 = nums\n 1163│        .next()\n 1164│        .and_then(|s| s.parse().ok())\n     │...\n 1212│            \"schtasks /create failed: {}\",\n*1213│            String::from_utf8_lossy(&created.stderr).trim()\n 1214│        )));\n 1215│    }\n 1216│    let ran = run(&[\"/run\", \"/tn\", &task_name]);\n     │...\n 1222│            \"schtasks /run failed: {}\",\n*1223│            String::from_utf8_lossy(&ran.stderr).trim()\n 1224│        )));\n 1225│    }\n 1226│    // No pid from schtasks; sentinel — readiness is the brain.ready poll.\n     │...\n 1274│    // NULL/absent handle (DETACHED_PROCESS rungs) all SURVIVE — nulling them is\n*1275│    // wrong (blanks a drained capture) and pointless (they never block). RED-FIRST:\n 1276│    // returning `true` unconditionally makes the non-pipe cases go red — which is\n*1277│    // exactly the mass int-test brain-log-blanking regression the file-type gate avoids.\n 1278│    #[test]\n 1279│    fn null_std_handles_only_on_a_pipe_stderr() {\n 1280│        const FILE_TYPE_UNKNOWN: u32 = 0x0000;\n     │...\n 1299│    // and caps at NET_RETRY_CAP — so a long boot-race wait settles to a steady\n*1300│    // 30s poll, never an ever-growing or zero-length spin.\n 1301│    #[test]\n 1302│    fn net_retry_backoff_doubles_then_caps() {\n 1303│        assert_eq!(net_retry_backoff(NET_RETRY_FIRST), Duration::from_secs(2));\n     │...\n 1309│    // [unit->REQ-HAZARD-DETACHED-PIPE-INHERIT] the no-inherit spawn's command\n*1310│    // line obeys CommandLineToArgvW quoting: bare words pass through; spaces\n 1311│    // quote; embedded quotes and the backslash-before-quote cases escape per\n 1312│    // the rules (a wrong quoting would mis-tokenize the shell spawn template).\n 1313│    #[cfg(windows)]\n     │...\n 1607│                if let Ok(s) = std::fs::read_to_string(&pidfile) {\n*1608│                    if let Ok(p) = s.trim().parse::<u32>() {\n 1609│                        if p != 0 {\n 1610│                            found = Some(p);\n 1611│                            break;\n### deelevate.rs\n   9│//! sudo flips `$HOME` — can mint a *different node identity* in root's\n *10│//! universe (the key-flip behind the 4.10 ghost rows).\n  11│//!\n  12│//! The invariant: **the daemon always runs unelevated in the invoking\n  13│//! user's universe, regardless of which command spawns it.** Two\n    │...\n  31│//! The OS probes mirror `spt`'s REQ-PAIR-6 `elevation.rs` (the CLI gate);\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    │...\n 108│        let key = k.to_ascii_uppercase();\n*109│        entries.retain(|e| name_of(e) != key);\n 110│        entries.push(format!(\"{k}={v}\").encode_utf16().collect());\n 111│    }\n 112│    let mut out: Vec<u16> = Vec::new();\n    │...\n 198│        let name = std::fs::read_to_string(path).ok()?;\n*199│        user_by_name(name.trim())\n 200│    }\n 201│\n 202│    /// Resolve a username to a drop target via `getpwnam_r`. Root refuses —\n    │...\n 319│        std::io::stdin().read_line(&mut s).ok()?;\n*320│        let s = s.trim().to_string();\n 321│        (!s.is_empty()).then_some(s)\n 322│    }\n 323│\n*324│    /// The uid's home dir via `getpwuid_r` (reentrant; buffer grown on\n 325│    /// ERANGE). `None` on any failure — the caller then leaves `HOME`\n 326│    /// alone rather than guessing.\n 327│    fn home_of(uid: u32) -> Option<PathBuf> {\n    │...\n 873│            got.iter()\n*874│                .filter(|e| e.to_ascii_uppercase().starts_with(\"SPT_HOME=\"))\n 875│                .count(),\n 876│            1\n 877│        );\n### digest.rs\n 139│        .map(|i| i.session_id.clone())\n*140│        .filter(|s| !s.is_empty());\n 141│\n 142│    // Resolve the adapter's manifest (with profile) by the perch's recorded\n 143│    // adapter. The record's `source_dir` is the adapter's install dir — where a\n    │...\n 189│        // Collapse cross-generation replay dupes on the RAW span, before the\n*190│        // two-origin merge + window fold, so a checkpoint/resume row surfaces once\n 191│        // (under its live generation) not twice. [impl->REQ-DIGEST-GENERATION-SUPERSEDE]\n 192│        supersede_cross_generation(spanned)\n 193│    } else {\n    │...\n 227│    // (session_id, ts, boundary-kind, ordinal) oldest→newest. A fresh/pre-ledger\n*228│    // perch has no row → the current bound session anchors at ordinal 0.\n 229│    let sessions: Vec<(String, Option<String>, String, u64)> = if ledger.is_empty() {\n 230│        current_session\n 231│            .map(|s| vec![(s.to_string(), None, \"boot\".to_string(), 0u64)])\n    │...\n 322│/// A checkpoint/resume replays an ancestor generation's transcript verbatim, so a\n*323│/// replayed row is BYTE-identical on exactly these fields; NUL-joined so distinct\n 324│/// field boundaries never collide.\n 325│// [impl->REQ-DIGEST-GENERATION-SUPERSEDE]\n 326│fn logical_key(r: &spt_term::DigestRecord) -> String {\n    │...\n 343│/// harness REPLAY the prior generation's transcript into the new session file, so\n*344│/// one logical row surfaces under two `seq`-generations — `(gen_old, localseq)`\n 345│/// and `(gen_new, localseq)` — and exact-seq consumer dedup can't collapse them.\n 346│/// Keep only the NEWEST-ordinal occurrence of each logical record (the surviving\n 347│/// `seq` is the live generation, so snapshot + `--follow` agree). Cross-generation\n    │...\n 349│/// replay itself) and MUST survive. Boundary dividers left adjacent to a\n*350│/// now-emptied ancestor are trimmed so no orphaned divider remains.\n 351│// [impl->REQ-DIGEST-GENERATION-SUPERSEDE]\n 352│fn supersede_cross_generation(items: Vec<spt_term::TimelineItem>) -> Vec<spt_term::TimelineItem> {\n 353│    use spt_term::TimelineItem;\n    │...\n 381│\n*382│    // Pass 3: keep a Boundary only BETWEEN two sessions that each retain ≥1 activity\n*383│    // row. A leading boundary (ancestor fully superseded → emptied), a trailing\n*384│    // boundary, and consecutive boundaries (a middle session emptied) are trimmed;\n 385│    // consecutive → the LAST wins (the divider INTO the surviving newer session).\n 386│    let mut out: Vec<TimelineItem> = Vec::with_capacity(kept.len());\n 387│    let mut seen_activity = false;\n    │...\n 424│    for (ln, raw) in s.lines().enumerate() {\n*425│        if raw.trim().is_empty() || spt_term::parse_context_record(raw).is_some() {\n*426│            continue; // blank, or an spt context entry (merged separately)\n 427│        }\n 428│        match spt_term::record_to_tagged_result(raw) {\n 429│            Ok(rec) => {\n    │...\n 535│        let a = vec![turn(\"a\"), turn(\"b\")];\n*536│        let grown = vec![turn(\"a\"), turn(\"b\"), turn(\"c\")];\n*537│        assert_eq!(common_prefix_len(&a, &grown), 2, \"shared head of two\");\n 538│        let slid = vec![turn(\"b\"), turn(\"c\"), turn(\"d\")];\n 539│        assert_eq!(common_prefix_len(&a, &slid), 0, \"front eviction → 0\");\n 540│    }\n    │...\n 682│    // [unit->REQ-DIGEST-GENERATION-SUPERSEDE] a checkpoint/resume replays gen A's\n*683│    // rows into gen B at the same localseq → the span carries each logical row\n 684│    // twice (two ordinals). Supersede keeps ONE, under B's (newer) generation, and\n 685│    // the surviving seq is B's.\n 686│    #[test]\n    │...\n 688│        let items = vec![\n*689│            sup_act(\"shared row\", \"2026-06-13T21:00:00Z\", 0, 0), // gen A\n 690│            sup_bound(),\n*691│            sup_act(\"shared row\", \"2026-06-13T21:00:00Z\", 1, 0), // gen B replay (same ts/text/localseq)\n*692│            sup_act(\"B tail\", \"2026-06-13T21:00:05Z\", 1, 1),     // B's own new row\n 693│        ];\n 694│        let out = supersede_cross_generation(items);\n 695│        assert_eq!(\n 696│            texts(&out),\n*697│            vec![\"shared row\", \"B tail\"],\n*698│            \"the replayed row appears ONCE, plus B's tail (no phantom double)\"\n 699│        );\n*700│        // The surviving \"shared row\" carries B's generation (ordinal 1), so a\n 701│        // consumer's exact-seq cursor tracks the live gen.\n 702│        let seq = out.iter().find_map(|it| match it {\n### digesthub.rs\n 130│        st.snapshot = new.clone();\n*131│        st.subscribers.retain(|tx| tx.send(update.clone()).is_ok());\n 132│        (new, st.version)\n 133│    }\n 134│\n    │...\n 419│\n*420│/// Collapse internal whitespace/newlines to a single spaced line for glanceable\n 421│/// rendering (the digest is a summary, not a transcript).\n 422│fn one_line(s: &str) -> String {\n*423│    s.split_whitespace().collect::<Vec<_>>().join(\" \")\n 424│}\n 425│\n 426│#[cfg(test)]\n    │...\n 462│\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### dispatch.rs\n  14│//! The stream table holds locally-opened outbound streams too. The dispatcher\n *15│//! claims only rows with `initiated_locally == false` — serving our own\n  16│//! requester stream would deadlock the requester against ourselves. The flag\n  17│//! is broker-recorded at registration (`open_stream` vs the accept loop),\n  18│//! never inferred.\n    │...\n  33│//! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |\n *34│//! | `kind=row` | notif feed ([`crate::notifsync::apply_notif_feed`]) |\n  35│//! | `kind=fetch` / `kind=push` | file transfer (D9-1-3) |\n  36│//! | untagged + `target`+`op_id` | WAN message feed ([`crate::wan::receive_wan`]) |\n  37│//! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |\n    │...\n  95│    Update,\n *96│    /// A notif replication feed (`NotifRecord::Row`).\n  97│    Notif,\n  98│    /// A WAN message feed (untagged `WanMessage` lines).\n  99│    WanMsg,\n    │...\n 134│        Some(\"status_query\") => StreamFamily::Update,\n*135│        Some(\"row\") => StreamFamily::Notif,\n 136│        Some(\"fetch\") | Some(\"push\") => StreamFamily::Xfer,\n 137│        Some(_) => StreamFamily::Unknown,\n 138│        None => {\n    │...\n 147│                // A node-level label feed (REQ-SUBNET-3): an endpoint-less peer\n*148│                // sends label records first (no instance rows), so the feed's\n 149│                // first line is a NodeLabelUpdate. Same serve arm, same gate.\n 150│                StreamFamily::Registry\n 151│            } else {\n    │...\n 295│\n*296│/// PURE claim decision: may this row be (re)claimed now? `Some(prior\n 297│/// failures)` = claim it; `None` = leave it (in flight, terminal, or backing\n 298│/// off). The unit-tested seam for the no-hot-loop ruling.\n 299│// [impl->REQ-DISPATCH-CLAIM-RETRY]\n    │...\n 308│/// Strike budget for a deadline-poisoned ONE-WAY replay (ADR-0040 decision 4,\n*309│/// REQ-REGISTRY-REPLAY-BOUNDED): at budget the row retires TERMINAL — safe by\n 310│/// family semantics (a feed is an idempotent snapshot advertisement; the next\n 311│/// pump round re-advertises, so worst-case loss is one ~30s round, already\n 312│/// the family's freshness unit). Request/reply families are NEVER on this\n    │...\n 318│/// (ADR-0040 decision 4). Registry only: a feed is idempotent and\n*319│/// re-advertised every round. Notif/WanMsg feeds carry durable rows and\n 320│/// replies respectively — dropping one loses real state, so they keep\n 321│/// request/reply semantics here even though their transport is feed-shaped.\n 322│// [impl->REQ-REGISTRY-REPLAY-BOUNDED]\n    │...\n 327│/// PURE outcome transition: what a finished worker's claim becomes, plus\n*328│/// whether the row must ALSO retire terminal (the one-way budget path).\n 329│/// Served and Unknown (unclassifiable) are TERMINAL — a terminal\n 330│/// classification never hot-loops. A failure requeues with backoff until the\n 331│/// bounded budget is spent, then goes terminal.\n    │...\n 341│/// Registry poisons). Its Transient budget exhaustion retires terminal too —\n*342│/// every terminal-from-failure one-way row leaves the table for good; the\n 343│/// next advertisement round is the recovery. Defense-in-depth behind\n 344│/// REQ-ONEWAY-STREAM-TERMINAL (which removes the population at source).\n 345│// [impl->REQ-DISPATCH-CLAIM-RETRY]\n    │...\n 397│/// PURE lifecycle+family discriminator (gate round 1, ADR-0038 Decision 1's\n*398│/// \"excluded by lifecycle state\" half): is a `finished` row TERMINAL for\n 399│/// redispatch — retire unserved, never replay?\n 400│///\n 401│/// `finished` alone is NOT the answer, and a bare `s.finished` claim filter\n    │...\n 417│// [impl->REQ-HAZARD-REDISPATCH-CONTROL-STEAL]\n*418│fn finished_row_is_terminal(family: StreamFamily, finished: bool) -> bool {\n 419│    finished && family == StreamFamily::Attach\n 420│}\n 421│\n    │...\n 455│/// unlike the bare pre-classification valve above) — proving strikes\n*456│/// accumulate per stream and the budget retires the row TERMINAL while\n 457│/// fresh feeds keep serving. Counted latch, armed once per process.\n 458│fn take_injected_registry_seatblocked() -> bool {\n 459│    use std::sync::atomic::AtomicUsize;\n    │...\n 520│    // Bounded worker pool (Amendment fix 7): a cold table (daemon refresh\n*521│    // with a large backlog) claims at most this many rows per poll — the\n 522│    // rest wait for the next tick, batching the cold enumeration instead of\n*523│    // spawning one thread per row. No claim lock is held across any I/O:\n 524│    // enumeration runs lock-free, the claim map is touched in short scopes.\n 525│    // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n 526│    let active_workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));\n    │...\n 537│        for s in reply.streams {\n*538│            // Claim only peer-initiated rows with bytes (a QUIC stream is\n 539│            // invisible until its first write anyway — gotcha #6 — but the\n*540│            // next_seq guard also skips a row whose pump hasn't appended yet).\n*541│            // Terminal rows never re-enter; a released (Retry) claim re-enters\n 542│            // after its backoff (REQ-DISPATCH-CLAIM-RETRY).\n 543│            if s.initiated_locally || s.next_seq == 0 {\n 544│                continue;\n    │...\n 578│                    // Retirement (ADR-0038 A, REQ-REDISPATCH-FINISHED-RETIRE):\n*579│                    // this stream's exchange is over — take its row out of\n 580│                    // every FUTURE dispatcher generation's enumeration (the\n 581│                    // KNOWN-HAZARDS 7.41 control-steal class). Best-effort:\n 582│                    // an older broker without the verb answers error (KH-2.3)\n*583│                    // and the row retires when the broker cycles.\n 584│                    if let Ok(mut retire_brain) = connect(&name) {\n 585│                        let _ = retire_brain.net_stream_retire(stream_id);\n 586│                    }\n### docshost.rs\n  39│    if let Some(v) = env_value {\n *40│        if let Ok(p) = v.trim().parse::<u16>() {\n  41│            if p != 0 {\n  42│                return p;\n  43│            }\n    │...\n  86│    }\n *87│    let trimmed = decoded.trim_start_matches('/');\n *88│    let rel = if trimmed.is_empty() {\n  89│        \"index.html\"\n  90│    } else {\n *91│        trimmed\n  92│    };\n  93│    let candidate = PathBuf::from(rel);\n  94│    for comp in candidate.components() {\n    │...\n 237│            // Serve one connection to completion; an error here is one\n*238│            // browser's problem, never the server's.\n 239│            let _ = hyper::server::conn::http1::Builder::new()\n 240│                .serve_connection(io, service)\n 241│                .await;\n### effect.rs\n   7│//! kills it. Effects it drives cross the broker↔brain boundary — a PTY write, a\n  *8│//! spool row, a registry update. A brain that crashes *around* one of those\n   9│//! effects must, on restart, neither duplicate it (a re-injected keystroke, a\n *10│//! double-written row) nor drop it (a lost delivery).\n  11│//!\n  12│//! ## The journal lines (`PENDING` / `DONE`) — and why a `PENDING`-no-`DONE` is normal\n  13│//!\n    │...\n  21│//! shape of an effect that did not complete: for a `net-send`, an **unconfirmed\n *22│//! write to an offline/unreachable peer**, retained as the re-drive recovery\n  23│//! marker (it re-drives on the next brain restart / reachability). Two reader\n  24│//! caveats: the log is **append-only**, so a failed-then-re-driven effect keeps\n  25│//! its stale `PENDING` line AND appends a fresh `PENDING`+`DONE` — a raw\n    │...\n  81│    PtyWrite,\n *82│    /// A spool-row write (message delivery durability).\n  83│    Spool,\n  84│    /// A registry / `info.json` mutation.\n  85│    Registry,\n    │...\n  90│    NetDial,\n *91│    /// A QUIC stream open or stream write (D4b) — same anchor, same namespace\n  92│    /// as [`EffectKind::NetDial`] (one durable op-id counter across net ops).\n  93│    NetSend,\n  94│}\n    │...\n 133│/// journal line is self-describing (see [`Minter::as_tag`]).\n*134│// [impl->REQ-OPID-MINTER-NAMESPACE]\n 135│#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\n 136│#[serde(rename_all = \"snake_case\")]\n 137│pub enum Minter {\n    │...\n 150│    /// SOURCE (the seq's counter origin), never the subsystem the call lives in —\n*151│    /// a `wake` op minted from `now_ms()` is not the `shell` spool-row counter\n 152│    /// (doyle ruling: subsystem-based tagging is the named anti-pattern).\n 153│    pub fn as_tag(self) -> &'static str {\n 154│        match self {\n    │...\n 293│/// produced the op, and the source's durable op id (a PTY-write sequence, a\n*294│/// spool-row id, …). The dedup key — the same logical effect re-driven after a\n 295│/// crash carries the *same* key, so the applied-set recognizes the replay. The\n 296│/// `minter` dimension (ADR-0034) stops two counters that both start at 1 from\n*297│/// colliding in the one journal namespace.\n 298│#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n 299│pub struct EffectKey {\n 300│    pub class: u64,\n    │...\n 523│        let line = line?;\n*524│        let mut parts = line.split_whitespace();\n 525│        match parts.next() {\n 526│            Some(\"PENDING\") => {\n 527│                // OLD: `PENDING {class} {op} {kind}` (3 tokens after marker) → Legacy.\n    │...\n 545│            }\n*546│            _ => {} // blank / partial / unknown line — skip\n 547│        }\n 548│    }\n 549│    Ok((applied, pending))\n    │...\n 646│            .apply_once(key, EffectKind::PtyWrite, || {\n*647│                *runs.borrow_mut() += 1;\n 648│                Ok(())\n 649│            })\n 650│            .unwrap();\n    │...\n 652│            .apply_once(key, EffectKind::PtyWrite, || {\n*653│                *runs.borrow_mut() += 1;\n 654│                Ok(())\n 655│            })\n 656│            .unwrap();\n    │...\n 663│        );\n*664│        assert_eq!(*runs.borrow(), 1, \"the effect ran exactly once\");\n 665│        assert!(j.is_applied(key));\n 666│    }\n 667│\n    │...\n 744│            .apply_once(key, EffectKind::Registry, || {\n*745│                *runs.borrow_mut() += 1;\n 746│                Ok(())\n 747│            })\n 748│            .unwrap();\n 749│        assert_eq!(o, Outcome::Deduped);\n*750│        assert_eq!(*runs.borrow(), 0, \"no re-run after recovery\");\n 751│    }\n 752│\n 753│    // ---- The Spike #6 gauntlet, on the brain-crash model -------------------\n    │...\n 794│                .apply_once(key, kind_for(op), || {\n*795│                    effects.borrow_mut().push(op);\n 796│                    Ok(())\n 797│                })\n 798│                .expect(\"apply_once\");\n    │...\n 911│            j.apply_once(pty_key, EffectKind::PtyWrite, || {\n*912│                *runs.borrow_mut() += 1;\n 913│                Ok(())\n 914│            })\n 915│            .unwrap(),\n    │...\n 920│            j.apply_once(net_key, EffectKind::NetSend, || {\n*921│                *runs.borrow_mut() += 1;\n 922│                Ok(())\n 923│            })\n 924│            .unwrap(),\n    │...\n 926│        );\n*927│        assert_eq!(*runs.borrow(), 0, \"neither replay re-ran its effect\");\n 928│\n 929│        // FILE CONTENT proxy for fsync: durable NetSend is on disk; ephemeral\n 930│        // PtyWrite is not. Keys are written as \"sid op\" tokens on the journal line.\n### endpoint.rs\n  99│    let s = std::fs::read_to_string(pump_heartbeat_path()).ok()?;\n*100│    serde_json::from_str::<u64>(s.trim()).ok()\n 101│}\n 102│\n 103│/// Read the pump's reachability health snapshot (`identity/pump-health.json`,\n### grants.rs\n  22│//! (M5-PLAN scope decision 2): the ids exist so the store/gate shape is final,\n *23│//! but [`decide`] refuses them **unconditionally — even if a grant row\n *24│//! exists** (defense in depth: a stray row authored today must not become a\n  25│//! live authorization the day the capability lands; landing it is an explicit\n  26│//! code change that removes the id from [`RESERVED_CAPABILITIES`]).\n  27│// [impl->REQ-CONSENT-1]\n    │...\n  42│/// `require_approval = \"remembered\"` allow-always answer writes a\n *43│/// `spawn-shell × agent × (node, shell-adapter)` row (qualifier = adapter).\n  44│pub const CAP_SPAWN_SHELL: &str = \"spawn-shell\";\n  45│/// A shell suspending its linked owner directly (`api owner-shutdown`) —\n  46│/// pre-consented via the shell manifest's `can_shutdown` flag (D1c authoring\n    │...\n  50│/// Capability ids whose *gate shape* ships in M5 but whose behavior is\n *51│/// deferred — refused unconditionally regardless of grant rows.\n  52│pub const RESERVED_CAPABILITIES: &[&str] = &[CAP_REMOTE_EXEC, CAP_INSTANTIATE_ANYWHERE];\n  53│\n  54│/// The gate's verdict for one `(capability, agent, node, qualifier)` ask.\n    │...\n  56│pub enum GrantDecision {\n *57│    /// A grant row covers the ask — proceed.\n  58│    Allowed,\n  59│    /// No grant — route an interactive escalation to the carried\n  60│    /// most-recently-active session (allow-once / allow-always / deny, D1b).\n    │...\n  71│/// The pure grant gate (REQ-CONSENT-1). Order: reserved-refusal first (defense\n *72│/// in depth — a stray row for a deferred capability is inert), then the store\n  73│/// lookup (exact-tuple, default-deny), else escalation with the\n  74│/// caller-resolved `target`. No I/O, no clock.\n  75│// [impl->REQ-CONSENT-1]\n    │...\n 137│    /// (`consent-needed cap=… agent=… node=… [qual=…]`) + a human hint.\n*138│    /// Fields are SPT ids / pubkey hex / adapter names — whitespace-free by\n*139│    /// charset (REQ-HAZARD-ID-CHARSET), so space-separated `key=value` holds.\n 140│    // [impl->REQ-CONSENT-2]\n 141│    pub fn body(&self) -> String {\n 142│        let qual = self\n    │...\n 167│        let line = body.lines().next()?;\n*168│        let mut parts = line.split_whitespace();\n 169│        if parts.next()? != ESCALATION_PREFIX {\n 170│            return None;\n 171│        }\n    │...\n 209│    now_ms: u64,\n*210│) -> Result<(spt_store::notif::NotifRow, crate::notif::FirstFireOutcome), String> {\n 211│    crate::notif::produce_and_first_fire(\n 212│        store,\n 213│        node_hex,\n    │...\n 232│\n*233│/// The qualifier narrowing a flag-authored `instantiate-anywhere` row to the\n 234│/// shell-wake path: `shell_wake_spawn_anywhere` pre-consents shells to\n 235│/// fresh-spawn **their owner, to wake it** — not general remote\n 236│/// instantiation (CONTEXT §Shell sleep/wake).\n    │...\n 262│/// Author the `shell_wake_spawn_anywhere` pre-consent for `owner`: an\n*263│/// `instantiate-anywhere` row qualified to the shell-wake path. **Flag shape\n 264│/// only in M5** — the capability is reserved, so [`decide`] still refuses it\n*265│/// unconditionally (M5-PLAN scope decision 2); the row becomes live the day\n 266│/// instantiate-anywhere lands and leaves [`RESERVED_CAPABILITIES`]. This\n*267│/// deliberate authoring path is the one sanctioned way such a row exists\n 268│/// (the CLI refuses casual reserved-id authoring).\n 269│// [impl->REQ-CONSENT-2]\n 270│pub fn author_wake_spawn_anywhere_grant(store: &mut GrantStore, owner: &str, node: &str) {\n    │...\n 278│    AllowOnce,\n*279│    /// Proceed and write the grant row — future asks pass the gate silently.\n 280│    AllowAlways,\n*281│    /// Refuse — no row, the action does not proceed.\n 282│    Deny,\n 283│}\n 284│\n    │...\n 289│    ProceedOnce,\n*290│    /// Proceed; the grant row is in `store` (caller persists it).\n 291│    ProceedGranted,\n 292│    /// Do not proceed; nothing persisted.\n 293│    Denied,\n    │...\n 334│    // [unit->REQ-CONSENT-1] default-deny: empty store escalates (with the\n*335│    // resolved target attached); a grant row flips the same ask to Allowed;\n 336│    // revocation flips it back.\n 337│    #[test]\n 338│    fn ungranted_escalates_granted_allows() {\n    │...\n 391│    // [unit->REQ-CONSENT-1] reserved capabilities refuse unconditionally —\n*392│    // even with a grant row present (defense in depth) and even with a live\n 393│    // escalation target available: neither grants nor escalation are\n 394│    // consulted for a deferred capability.\n 395│    #[test]\n    │...\n 447│        );\n*448│        assert!(s.rows.is_empty(), \"once never persists\");\n 449│\n 450│        assert_eq!(\n 451│            apply_escalation_answer(&mut s, &a, EscalationAnswer::Deny),\n\n### bin/\n#### xlate_choreo_fixture.rs\n  54│        let Ok(line) = line else { break }; // stdin closed (EOF) → exit (no-zombie).\n *55│        let trimmed = line.trim();\n *56│        if trimmed.is_empty() {\n  57│            continue;\n  58│        }\n  59│\n    │...\n  63│            if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) {\n *64│                let _ = writeln!(f, \"{trimmed}\");\n  65│            }\n  66│        }\n  67│\n    │...\n  69│        // content-free `input` ping are noted (logged above) and ignored.\n *70│        let v: serde_json::Value = match serde_json::from_str(trimmed) {\n  71│            Ok(v) => v,\n  72│            Err(_) => continue,\n  73│        };\n    │...\n 142│/// would false-pass). ~12 lines of ~70 cols ≈ 850 B — larger than the field's ~322 B\n*143│/// truncation and many settle-chunks wide, yet it fits inside a 24-row screen grid so a\n 144│/// cold-attach repaint (ADR-0031 = current screen) still carries the HEAD (a taller\n 145│/// payload scrolls HEADSTART off the visible grid).\n 146│fn big_multiline_payload() -> String {"},"isError":false,"timestamp":1784365474695}}
{"type":"message","id":"d66e5aea","parentId":"eb5d7588","timestamp":"2026-07-18T09:04:34.713Z","message":{"role":"toolResult","toolCallId":"call_NnnzrjeNM1MICXTJBAIDXjHw|fc_0ce5441d29b9f05d016a5b41a28dd0819b9117604b205e79c9","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/\n## Cargo.toml\n 3|version.workspace = true\n*4|edition = \"2021\"\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|# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for\n*9|# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /\n 10|# REQ-STREAM-OPENER-DURABLE / REQ-DISPATCH-CLAIM-RETRY): the REAL\n*11|# `run_dispatch_loop` as a KILLABLE child process — a killed generation drops\n 12|# its workers' broker conns exactly like a production brain death. Same\n*13|# tests/-homed `[[bin]]` pattern as spt's fixtures (excluded from the release\n 14|# build, which is `--bin spt` only). Name avoids the substring \"update\"\n 15|# (the Windows installer-detection UAC-740 class).\n 16|[[bin]]\n...\n 20|[dependencies]\n*21|# Layer-below set (R-ARCH-1 acyclic): …→spt-live→spt-daemon→spt. The broker\n*22|# re-hosts spt-term PTYs + the spt-live lifecycle seams; these edges fix the\n 23|# crate's position in the workspace layering. Tasks B1–B8 consume them.\n 24|spt-proto = { path = \"../spt-proto\" }\n 25|spt-store = { path = \"../spt-store\" }\n...\n 34|spt-net = { path = \"../spt-net\" }\n*35|# The broker is otherwise sync/thread-based; iroh requires tokio, so the broker\n*36|# hosts a dedicated runtime on named worker threads (QUIC objects never leave it).\n 37|tokio = { version = \"1\", features = [\"rt-multi-thread\", \"net\"] }\n 38|# The node-local docs server (ADR-0036 §4, REQ-DOCS-LOCAL-SERVER): hyper +\n*39|# its util crates are ALREADY in the lock via iroh/reqwest — battle-tested\n*40|# crate over hand-rolled HTTP (operator-ruled), zero new supply chain.\n 41|hyper = { version = \"1\", features = [\"server\", \"http1\"] }\n 42|hyper-util = { version = \"0.1\", features = [\"tokio\"] }\n 43|http-body-util = \"0.1\"\n...\n 45|serde_json = { version = \"1\", features = [\"preserve_order\"] }\n*46|# PTY output is arbitrary bytes; the forward-compat JSON envelope carries it as\n 47|# a base64 string in the payload rather than forking the codec for binary.\n 48|base64 = \"0.22\"\n*49|# Cross-platform local IPC: named pipe (Windows) / Unix-domain socket (unix)\n 50|# behind one API — the OS-split substrate the DaemonTransport trait wraps,\n 51|# mirroring how spt-term abstracts ConPTY vs forkpty behind SessionSurface.\n 52|interprocess = \"2\"\n*53|# Self-update release signing (C1, REQ-UPD-2): pure-Rust Ed25519 — no C/asm\n 54|# build deps, clean cross-compile on win+linux (clean-room posture, ADR-0004 §D).\n*55|# `rand_core` lets a release-tooling caller mint a keypair; tests use the\n 56|# deterministic `from_bytes` ctor (no RNG).\n*57|ed25519-dalek = \"2\"\n*58|# SHA-256 binds the signed release metadata to the actual binary bytes\n 59|# (artifact digest) so the signature covers the binary, not just the manifest.\n 60|sha2 = \"0.10\"\n*61|# Per-frame link auth on the same-node shell channels (CONTEXT local-link\n*62|# authentication): HMAC-SHA256 over the token-derived link key (M5-D3b).\n 63|hmac = \"0.12\"\n 64|\n\n## src/\n### access.rs\n 2|//!\n*3|//! ADR-0009 models access as **three orthogonal, nested gates**, evaluated in\n 4|//! order: subnet *visibility* (is the endpoint routable here at all? —\n*5|//! `spt_store::visibility`, applied inside registry resolution) → the\n*6|//! **endpoint access whitelist** (may *your node* remotely reach it? — THIS\n 7|//! module) → *capability grants* (may this agent do this high-risk thing? —\n*8|//! [`crate::consent`], default-deny). The whitelist is the outer gate before\n*9|//! grants: an origin the whitelist refuses never reaches a grant check.\n 10|//!\n*11|//! ## Whitelist semantics (stateful firewall, not a blanket block)\n 12|//!\n 13|//! - **Origin-node gate:** the subject is the node the inbound interaction\n 14|//!   *originates from* (where the operator is acting) — never the sender\n...\n 16|//!   handshake-proven remote node id from the broker's conn/stream table,\n*17|//!   never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).\n*18|//! - **Outbound** → any visible node: the whitelist never restricts who an\n 19|//!   endpoint talks *to*.\n*20|//! - **Inbound reply** correlated to the endpoint's own prior outbound\n*21|//!   (keyed on the inbound `from`) → allowed from any visible node\n*22|//!   (\"established/related\").\n 23|//!   Classification reads the durable [`RecentOutbound`] log\n*24|//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].\n*25|//! - **Unsolicited/direct inbound** → only from a whitelisted node.\n*26|//! - **Same-node operation is always allowed** — the gate sits on the\n*27|//!   net-inbound path only, so the local delivery path never sees it; an\n*28|//!   origin that *is* this node (a loopback self-dial) short-circuits Allow.\n*29|//! - **Default absent = open**; setting a whitelist restricts (an empty entry\n*30|//!   = locked down). Node-tier ships M4 (Ed25519 node pubkeys); the user tier\n*31|//!   is an inert schema reservation until the cross-user model (ADR-0006).\n 32|//!\n 33|//! The store is [`spt_store::access::AccessStore`] (`trust/access.json` —\n 34|//! security material next to the trust store). The gate re-reads it per check:\n*35|//! an `spt access` edit takes effect on the next inbound without a daemon\n 36|//! restart, and the checks run at message/stream-establishment rate where a\n 37|//! small-file read is noise.\n 38|//!\n### adapter_update.rs\n*1|//! Adapter ripple-update conduct (C3) — spt-core updates the whole stack: self\n*2|//! first, then each registered adapter via its manifest `[update]` avenue, with\n 3|//! **adapter content signing** (REQ-UPD-5, REQ-SEAM-UPDATE, ADR-0004 §D).\n 4|//!\n 5|//! After spt-core's own update completes (the C0–C2 engine), it conducts a\n*6|//! ripple over its registered adapters. Each adapter declares how it updates in\n 7|//! its manifest `[update]` block ([`Update`]); this module turns that directive\n*8|//! into a per-adapter [`AdapterUpdateOutcome`], dispatched by avenue:\n 9|//!\n 10|//! - **`file_pull`** — spt-core controls the bytes, so it *verifies* them: the\n*11|//!   pulled payload is checked against the adapter's **own** Ed25519 key (declared\n*12|//!   in the manifest — the adapter author signs their releases; spt-core's release\n*13|//!   key stays scoped to spt-core) plus the SHA-256 artifact binding. Authentic +\n*14|//!   intact ⇒ [`AdapterUpdateOutcome::Verified`]; otherwise\n*15|//!   [`AdapterUpdateOutcome::Rejected`].\n*16|//! - **`delegated`** — the adapter's own updater installs the content (e.g.\n 17|//!   `claude.exe plugin update`); spt-core never sees those bytes, so there is\n*18|//!   nothing for it to hash. It delegates the trust **only** when the adapter\n*19|//!   attests `self_verifies` (its updater checks its own signatures) ⇒\n*20|//!   [`AdapterUpdateOutcome::Delegate`]; an unattested delegated update is\n*21|//!   [`AdapterUpdateOutcome::Skipped`] as unverifiable.\n 22|//!\n 23|//! The actual byte transport for `file_pull` (the network pull) is M4\n*24|//! (`REQ-UPD-1`); v1 receives the payload via self-fetch / out-of-band and feeds\n 25|//! it here. This module is the **decision + verification** layer — it does not\n*26|//! itself fetch or exec.\n 27|// [impl->REQ-UPD-5]\n 28|// [impl->REQ-SEAM-UPDATE]\n 29|\n...\n 31|\n*32|use crate::release::{\n*33|    parse_verifying_key, verify_artifact, verify_signature, RejectReason, ReleaseMetadata,\n*34|    SignedRelease,\n 35|};\n 36|\n*37|/// A pulled `file_pull` payload awaiting verification: the adapter's signed\n### applyhost.rs\n*1|//! Staged-update apply orchestration (M5-D7b) — the **production caller** for\n 2|//! [`crate::update::apply_brain_only`], closing the M4-D9 honest residual\n 3|//! (\"nothing in the daemon invokes this apply after the user acks the consent\n 4|//! notif\").\n...\n 9|//! notif and is a *prompt* answer, not a lock — an apply after a dismiss\n*10|//! still works, the user changed their mind).\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*16|//!    `release-keys.json` policy): apply never trusts the cache — a tampered\n*17|//!    or expired artifact refuses here. The monotonic-version floor was\n*18|//!    enforced at *stage* time (REQ-HAZARD-UPDATE-ROLLBACK); the apply-time\n 19|//!    re-check guards authenticity/integrity/expiry of the bytes on disk.\n 20|//! 2. **Classify** against the running broker's ABI. Only\n 21|//!    [`UpdateClass::BrainOnly`] applies in M5 — the broker-touching classes\n*22|//!    refuse typed, the artifact stays staged (their consent-and-scheduling\n 23|//!    orchestration is post-M5; ADR-0004 §A).\n 24|//! 3. **Swap the binary**: the current executable steps aside\n 25|//!    (`<exe>.old-<ver>` — a running image survives its own rename on both\n*26|//!    OSes) and the verified artifact lands at the executable path\n*27|//!    atomically. A failed landing rolls the original back — never a\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 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 41|// [impl->REQ-UPD-4]\n...\n 47|use crate::brain::{now_ms, Brain};\n*48|use crate::relcache::{AppliedRecord, ReleaseCache, StagedUpdate};\n*49|use crate::release::{current_platform, VerifyPolicy};\n*50|use crate::update::{plan_verified, plan_verified_update_set, BrokerAbi, UpdateClass};\n 51|\n*52|/// What [`apply_staged`] did — loud + typed, never silent.\n 53|#[derive(Debug, Clone, PartialEq, Eq)]\n### attach.rs\n 3|//!\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*9|//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n 10|//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n*11|//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n*12|//!   journal (a replayed input record cannot double-type);\n 13|//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n 14|//!\n*15|//! ## Exactly-once lives at the EDGES, not the wire sends\n 16|//!\n*17|//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\n*18|//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n*19|//! and *discards* everything interleaved — inside a full-duplex pump loop that\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*25|//! re-transmit output records on the wire — duplicates are dropped at the\n*26|//! render cursor, the same discipline the local `OutputLog` replay uses\n 27|//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n 28|//!\n 29|//! **Compute + files stay here**; the operator is a viewport (CONTEXT\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*35|//! the D5d whitelist drops in at this same call.\n 36|//!\n 37|//! Restart survival composes from D4/B5 with **no new machinery**: the broker\n 38|//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n*39|//! A restarted target brain re-attaches the session from its output cursor,\n*40|//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n*41|//! a restarted operator brain resubscribes and dedups rendered chunks by\n 42|//! `Output.seq`.\n 43|\n 44|use std::io;\n### autostart.rs\n 4|//! always-there, but a box reboot / daemon cold start left it down until\n*5|//! hands-on (deployah field-confirm 2026-07-10, post-v0.30.6 restart). The\n 6|//! operator marks an endpoint a **startup default** with `spt endpoint run\n*7|//! --save` (the endpoint analog of the shipped `subnet attach --save`\n*8|//! precedent — doyle-ruled shape (a)); the daemon replays the saved runs here\n 9|//! at start.\n 10|//!\n 11|//! Contract: a replay is best-effort and LOUD — an unresolvable adapter or a\n*12|//! failed launch logs its skip and moves on; nothing here can block daemon\n 13|//! start or the other replays. Each replay is a FRESH session (shape (c)\n*14|//! restore-what-was-up was rejected): the adapter option re-resolves against\n*15|//! the live registry, the session id is freshly minted, and home rides the\n 16|//! endpoint's existing perch (durable — REQ-INST-15), so no `--subnet` is\n*17|//! recorded or needed.\n 18|\n 19|use crate::config::DaemonConfig;\n 20|\n*21|/// How long to keep retrying a launch whose broker round-trip failed — the\n 22|/// replay thread starts alongside the broker's serve thread, so the very first\n*23|/// connect can race the accept loop up. Bounded and short: a genuine launch\n 24|/// failure (dead adapter binary, bad template) fails again instantly and we\n 25|/// move on after the last attempt.\n 26|const LAUNCH_ATTEMPTS: u32 = 3;\n*27|const LAUNCH_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(750);\n 28|\n*29|/// Replay every saved startup default against the (just-bound) broker. Runs on\n 30|/// a daemon-boot background thread — never returns an error, never panics the\n 31|/// daemon; every outcome is a loud stderr token:\n*32|/// - `ENDPOINT_AUTOSTART:<id>` — the saved run came up;\n*33|/// - `ENDPOINT_AUTOSTART_SKIP:<id>` — the saved adapter no longer resolves to a\n*34|///   registered harness (the save is stale; re-run `endpoint run --save`);\n*35|/// - `ENDPOINT_AUTOSTART_FAIL:<id>` — the launch itself failed after retries.\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...\n 43|    // `{node}` fill: the same OS-hostname fallback the CLI spawn path uses —\n*44|    // the in-mem label handle lives daemon-side but this thread predates it.\n*45|    let node_label = spt_store::hostlabel::node_fill_label(None);\n 46|    for entry in entries {\n*47|        // Re-resolve the adapter option fresh (an adapter updated between\n*48|        // restarts is honored; an uninstalled one is a loud skip, never a wedge).\n 49|        let (record, manifest) =\n 50|            match spt_runtime::registry::resolve_option(&adapters_dir, &entry.adapter) {\n 51|                Ok((r, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => {\n...\n 55|                    eprintln!(\n*56|                        \"ENDPOINT_AUTOSTART_SKIP:{}: saved adapter '{}' is not a harness \\\n 57|                         adapter — re-save with `spt endpoint run --save`\",\n 58|                        entry.id, entry.adapter\n 59|                    );\n### brain.rs\n 2|//!\n*3|//! The brain holds all the volatile logic; it may be killed and restarted at\n*4|//! any moment (a routine self-update swaps it; a crash kills it). It connects to\n*5|//! the stable [`crate::broker::Broker`] over the versioned IPC, drives sessions\n 6|//! (spawn / input / resize / kill), and consumes their output. Two invariants\n 7|//! this type encodes:\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 21|//!   error — so a lost chunk can never pass silently.\n 22|// [impl->REQ-HAZARD-GEN-START-NOW]\n...\n 25|use std::io;\n*26|use std::sync::mpsc::{self, Receiver, RecvTimeoutError};\n*27|use std::thread::{self, JoinHandle};\n 28|use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n 29|\n*30|use interprocess::local_socket::prelude::*;\n 31|\n 32|use interprocess::local_socket::{SendHalf, Stream};\n 33|\n 34|use crate::codec::{read_frame, write_frame};\n*35|use crate::frame::{Envelope, Role};\n*36|use crate::effect::MintedOp;\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*42|    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n*43|    NetStreamSubscribeReq, NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, NetStreamUnsubscribeReq, NetStreamUnsubscribed, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n*44|    SessionsReply, SizeEvent, SpawnConflict, SpawnReq, Spawned, StreamLifetime, SubscribeOutcome,\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*5|//! drifted to an in-process thread (`daemon.rs`), so `spt update apply` swaps\n 6|//! the binary on disk but never restarts the running code (the regression\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 12|//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n*13|//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n*14|//! verb. The digest hub serve + seed-control stay broker-side continuity\n 15|//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n 16|//! loops.\n 17|//!\n...\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 25|use std::io;\n...\n 28|use std::sync::atomic::{AtomicBool, Ordering};\n*29|use std::sync::{Arc, OnceLock};\n 30|use std::thread;\n 31|use std::time::{Duration, Instant};\n 32|\n...\n 34|use crate::endpoint::{brain_ready_path, broker_socket_name};\n*35|use crate::relcache::{AppliedRecord, ReleaseCache};\n 36|\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 42|/// never a hot respawn loop.\n 43|const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n 44|/// A run at least this long counts as healthy and resets the backoff floor.\n*45|/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n 46|/// brain has this long to signal `ready` for its generation before the window\n*47|/// elapses (the \"up but useless\" alive-never-ready rollback path).\n 48|const SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\n 49|/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\n*50|/// **exits before** signaling ready is respawned this many consecutive times\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 56|/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\n...\n 59|/// How long the brain child retries connecting to the broker socket before\n*60|/// giving up (the broker may still be binding — the child is freshly spawned).\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### broker.rs\n*1|//! The broker kernel (B1 + B2) — the stable half of the broker/brain split.\n 2|//!\n 3|//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n*4|//! would lose continuity on if the logic restarted: the PTY master+writer and\n*5|//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n 6|//! client / listening sockets. It runs **no logic** — no routing, registry,\n*7|//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n 8|//! forward its output to the brain, inject the brain's input, report exit.\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 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*18|//!   [`OutputLog`]: every chunk gets a monotonic per-session `seq` and is held\n*19|//!   in a bounded ring. The current subscriber (a brain connection) receives\n 20|//!   chunks live; all subscriber writes happen *under the log lock*, so frames\n 21|//!   reach the brain in strict `seq` order with no interleave.\n*22|//! - **Resume from a cursor.** A reconnecting brain sends `subscribe` with its\n 23|//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\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 29|//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n 30|//! output log persist. A session leaves the table only when its child exits\n*31|//! (reaped by the exit waiter) or is explicitly killed.\n 32|// [impl->REQ-DAEMON-2]\n 33|\n 34|use std::collections::{HashMap, HashSet, VecDeque};\n...\n 36|use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};\n*37|use std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};\n*38|use std::sync::{Arc, Mutex, OnceLock};\n*39|use std::thread::{self, JoinHandle};\n 40|use std::time::{Duration, Instant};\n 41|\n*42|use interprocess::local_socket::{prelude::*, Stream};\n 43|use spt_term::{Drain, PtySession, ScreenGrid, SessionSurface, SurfaceSize};\n 44|\n 45|use spt_net::net::attach::AttachIntent;\n...\n 51|use crate::effect::{EffectJournal, EffectKey, EffectKind, Minter, Outcome};\n*52|use crate::frame::{Envelope, Role};\n 53|use crate::msg::{\n*54|    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,\n### codec.rs\n*1|//! Length-prefixed framing for the broker↔brain IPC.\n 2|//!\n 3|//! Frame: a 4-byte big-endian length prefix, then that many bytes of UTF-8\n*4|//! JSON encoding one [`Envelope`]. Same shape as `spt-msg::wire` (ADR-0001\n 5|//! framing) but generic over any [`Read`]/[`Write`] — the IPC carrier is a\n*6|//! local socket (named pipe / UDS), not a `TcpStream`, and tests exercise it\n 7|//! over in-memory buffers and socket pairs.\n 8|//!\n*9|//! The frame *codec* is deliberately transport-agnostic: it is the byte-level\n 10|//! contract, while [`crate::transport`] owns the OS-specific connection.\n 11|// [impl->REQ-DAEMON-2]\n 12|\n*13|use std::io::{self, Read, Write};\n 14|\n*15|use crate::frame::Envelope;\n 16|\n*17|/// Maximum framed message size accepted (16 MiB) — guards a garbage/hostile\n 18|/// length prefix from forcing a huge allocation. Matches `spt-msg::wire`.\n 19|const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;\n 20|\n*21|/// Serialize and write one [`Envelope`] as a length-prefixed JSON frame.\n*22|pub fn write_frame<W: Write>(w: &mut W, env: &Envelope) -> io::Result<()> {\n 23|    let body =\n 24|        serde_json::to_vec(env).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 25|    if body.len() > MAX_FRAME_BYTES {\n...\n 37|\n*38|/// Read one length-prefixed JSON frame and decode it into an [`Envelope`].\n*39|/// Rejects an oversized length prefix before allocating. A clean EOF on the\n*40|/// length prefix surfaces as [`io::ErrorKind::UnexpectedEof`] (peer hung up).\n*41|pub fn read_frame<R: Read>(r: &mut R) -> io::Result<Envelope> {\n 42|    let mut len_buf = [0u8; 4];\n 43|    r.read_exact(&mut len_buf)?;\n 44|    let len = u32::from_be_bytes(len_buf) as usize;\n...\n 64|    fn frame_round_trips_through_buffer() {\n*65|        let env = Envelope::new(\"attach\", json!({ \"session\": \"s1\", \"n\": 7 }));\n 66|        let mut buf: Vec<u8> = Vec::new();\n 67|        write_frame(&mut buf, &env).unwrap();\n 68|\n*69|        let mut cursor = io::Cursor::new(buf);\n*70|        let got = read_frame(&mut cursor).unwrap();\n 71|        assert_eq!(got.protocol_version, IPC_PROTOCOL_VERSION);\n 72|        assert_eq!(got.kind, \"attach\");\n 73|        assert_eq!(got.payload, json!({ \"session\": \"s1\", \"n\": 7 }));\n...\n 75|\n*76|    // [unit->REQ-DAEMON-2] multiple frames stream back-to-back without bleed.\n 77|    #[test]\n 78|    fn back_to_back_frames_decode_independently() {\n 79|        let mut buf: Vec<u8> = Vec::new();\n*80|        write_frame(&mut buf, &Envelope::new(\"a\", json!(1))).unwrap();\n*81|        write_frame(&mut buf, &Envelope::new(\"b\", json!(2))).unwrap();\n 82|\n*83|        let mut cursor = io::Cursor::new(buf);\n### config.rs\n 3|//!\n*4|//! In the no-daemon interim model the pulse cadence was a hard-coded\n 5|//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\n*6|//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\n*7|//! period is **durable and configurable**, sourced from a small JSON file under\n 8|//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\n*9|//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing\n*10|//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence\n 11|//! from config, never a constant.\n 12|// [impl->REQ-DAEMON-1]\n 13|\n...\n 18|\n*19|/// The interim default pulse period carried forward as the config default. The\n 20|/// daemon now treats this as a *fallback*, not a constant: `daemon.json` may\n 21|/// override it per machine.\n 22|pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\n 23|\n*24|/// Default drop-file precedence protection window (epoch-ms), mirroring the\n*25|/// interim `spt_live` guard. Surfaced here so the one config source owns it.\n 26|pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;\n 27|\n*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 35|/// Default peer-pump cadences (D9-1): how often the daemon pushes its\n*36|/// registry/notif feeds to trusted peers and pulls context syncs from them.\n 37|/// Conservative defaults — every loop is a thin call over an idempotent\n*38|/// driver, so a missed tick costs only latency, never correctness.\n 39|pub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\n 40|pub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\n 41|pub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\n...\n 44|/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\n*45|/// authored by a node not heard within this window are evicted on the next\n*46|/// registry pump tick. Sized to ~10 default pump cadences — generous against\n*47|/// transient drops (a peer re-advertises every cadence, so one admitted feed\n 48|/// inside the window keeps its rows), short enough that a dead node identity\n 49|/// stops poisoning bare-id resolution within minutes.\n 50|pub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\n 51|\n*52|/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —\n*53|/// the `\"relay\"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in\n 54|/// `Daemon::run`; kept as a local enum so the config layer stays transport-free.\n*55|/// The self-hosted variant (`RelayPolicy::SelfHosted`) needs a relay-map URL\n### conn.rs\n*1|//! Bounded, cancelable, poison-on-failure framed writes for one physical\n*2|//! broker connection (UPDATE-WEDGE round-4).\n 3|//!\n 4|//! ## The failure class this kills\n 5|//!\n*6|//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\n 7|//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\n 8|//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n*9|//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n*10|//! A connected consumer that stops reading (an `rc --take` whose process\n*11|//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n*12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n*13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n*14|//! connection, so the writer (and every write queued behind the same gate)\n*15|//! stays wedged until the peer process dies (~128 s in the field capture, on a\n 16|//! brain restart).\n 17|//!\n 18|//! ## The primitive\n...\n 20|//! [`BrokerConn`] owns the send half outright — it never leaves this module,\n*21|//! so an unbounded bypass write cannot compile. Every write:\n 22|//!\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...\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### consent.rs\n*1|//! Self-update consent gating (C2) — the REQ-UPD-4 decision + the\n 2|//! most-recently-active-session resolver.\n 3|//!\n*4|//! ADR-0004 makes self-update **not fully automatic by default**: a verified,\n*5|//! eligible update is gated on a user confirmation delivered to the user's\n 6|//! **most-recently-active live session**, with an explicit opt-in to full-auto\n 7|//! (the seamless, prompt-free path). C2 ships the load-bearing, fully-testable\n 8|//! core of that:\n 9|//!\n*10|//! - [`decide`] — the **pure** gate: full-auto ⇒ [`ConsentDecision::AutoApproved`];\n*11|//!   otherwise [`ConsentDecision::NeedsConsent`] carrying the resolved target the\n 12|//!   orchestrator should prompt.\n 13|//! - [`most_recently_active`] — resolve that target over the **v1 registry**\n 14|//!   (live perches + their `last_active_ms` recency stamp). This is the\n*15|//!   registry-resolution precursor CONTEXT names; the deferred PresenceChannel\n 16|//!   later generalizes it.\n 17|//!\n*18|//! What C2 deliberately does **not** build: the interactive prompt UX itself.\n*19|//! That rides the deferred notification primitive (ADR-0007 — `update-available`\n*20|//! / `consent-needed` producers) and the harness's native confirm affordance\n*21|//! (e.g. AskUserQuestion). [`ConsentDecision::NeedsConsent`] is the typed seam\n 22|//! those plug into; nothing here reaches into a session.\n 23|// [impl->REQ-UPD-4]\n 24|\n...\n 26|\n*27|/// The session a consent prompt should be delivered to: the user's\n 28|/// most-recently-active live perch. Carries enough to address it (id + path) and\n*29|/// the recency that won it the selection (for logging / tie-break audit).\n 30|#[derive(Debug, Clone, PartialEq, Eq)]\n 31|pub struct ConsentTarget {\n 32|    /// The agent/perch id of the most-recently-active live session.\n 33|    pub id: String,\n*34|    /// Its resolved perch directory.\n 35|    pub perch_path: PathBuf,\n*36|    /// Its `last_active_ms` recency stamp (0 if never stamped — least-recent).\n 37|    pub last_active_ms: u64,\n 38|}\n 39|\n*40|/// The gate's verdict for a verified, eligible update (REQ-UPD-4).\n 41|#[derive(Debug, Clone, PartialEq, Eq)]\n 42|pub enum ConsentDecision {\n*43|    /// Full-auto is opted in — apply without a prompt (the seamless path).\n*44|    AutoApproved,\n*45|    /// Default-gated — hold the update until the user confirms. Carries the\n*46|    /// resolved session to prompt, or `None` when no live session could be\n*47|    /// resolved (the orchestrator then defers / falls back to a node-level surface\n### crc_swap.rs\n*1|//! W3b — content-hash-gated atomic per-file swap for a live adapter update\n 2|//! (ADR-0025, REQ-ADAPTER-LIVE-UPDATE).\n 3|//!\n 4|//! The legacy `spt adapter update` path does `remove_dir_all(install_dir)` then a\n*5|//! blanket extract — which fails on Windows with *\"Access is denied (os error 5)\"*\n*6|//! the moment a single install-dir binary is still running (the field bug). This\n*7|//! module replaces that with a **content-diff swap**: extract the staged archive\n 8|//! to a temp tree, replace in the install dir **only** the files whose content\n 9|//! hash differs (so identical files — and their still-running binaries — are\n*10|//! never touched), and commit via **stage-all-`.new`-then-one-tight-rename-loop**\n 11|//! so a mid-swap failure never strands the install half-new (doyle W3 gate,\n 12|//! binding partial-failure refinement).\n 13|//!\n*14|//! CRC here is plain content hashing (reuse [`crate::release::sha256_hex`], no new\n*15|//! dep) — \"replace only what changed\", not cryptographic integrity (that is the\n 16|//! archive signature, ADR-0024 / REQ-UPD-9).\n 17|\n*18|use crate::release::sha256_hex;\n 19|use std::path::{Path, PathBuf};\n 20|\n*21|/// One planned file replacement discovered by [`plan_crc_swap`].\n 22|#[derive(Debug, Clone, PartialEq, Eq)]\n*23|pub struct PlannedSwap {\n*24|    /// Path relative to the tree root (for diagnostics / determinism).\n*25|    pub rel: PathBuf,\n*26|    /// The staged source file (`<staging>/<rel>`).\n*27|    pub staged: PathBuf,\n*28|    /// The install-dir target (`<install_dir>/<rel>`).\n 29|    pub target: PathBuf,\n 30|}\n 31|\n...\n 33|/// `install_dir` counterpart — or that are absent in `install_dir`. Files whose\n*34|/// content is byte-identical are skipped, so a still-running unchanged binary is\n*35|/// never disturbed. Read-only: does not mutate either tree. The returned plan is\n*36|/// sorted by `rel` for deterministic apply + testing.\n 37|// [impl->REQ-ADAPTER-LIVE-UPDATE]\n*38|pub fn plan_crc_swap(staging: &Path, install_dir: &Path) -> std::io::Result<Vec<PlannedSwap>> {\n 39|    let mut plan = Vec::new();\n 40|    collect_diffs(staging, staging, install_dir, &mut plan)?;\n*41|    plan.sort_by(|a, b| a.rel.cmp(&b.rel));\n 42|    Ok(plan)\n 43|}\n 44|\n### daemon.rs\n 3|//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds\n*4|//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the\n 5|//! canonical per-home names ([`crate::endpoint`]) and serves both for the life\n 6|//! of the process.\n 7|//!\n 8|//! [`ensure_running`] is the auto-start seam (REQ-DAEMON-3): any `api`\n*9|//! invocation calls it; if no daemon answers the seed-control ping it spawns\n*10|//! `spt daemon` detached and waits (bounded) for it to come up. The reliable\n*11|//! anchor is the seed channel — bound for the whole daemon lifetime, so a ping\n*12|//! against it is a true liveness test (`listen` is the interim model's anchor;\n*13|//! here the seed channel plays that role for cold-start detection).\n 14|// [impl->REQ-DAEMON-3]\n 15|\n 16|use std::io;\n...\n 22|\n*23|use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n 24|\n 25|use crate::broker::{default_journal_path, Broker};\n*26|use crate::config::{DaemonConfig, RelayChoice};\n*27|use crate::digesthub::{self, DigestHub};\n 28|use crate::endpoint::{\n*29|    broker_socket_name, daemon_pid_path, digest_socket_name, drive_socket_name, seed_socket_name,\n 30|};\n 31|use crate::nethost::{NetConfig, NetHost};\n*32|use crate::seedmap::{self, SeedRegistry};\n 33|\n*34|/// How long auto-start waits for a freshly-spawned daemon to bind its channel.\n 35|const START_TIMEOUT: Duration = Duration::from_secs(5);\n*36|/// Poll cadence while waiting for the spawned daemon to come up.\n 37|const START_POLL: Duration = Duration::from_millis(25);\n 38|\n 39|/// The per-machine broker/brain daemon process.\n...\n 42|impl Daemon {\n*43|    /// Run the daemon: bind the broker (served on a background thread) + the\n*44|    /// seed-control channel (the foreground loop + liveness anchor) and serve\n*45|    /// until the process is killed. The body of `spt daemon`.\n 46|    pub fn run() -> io::Result<()> {\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*52|        // is touched or any child can be spawned.\n 53|        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n 54|        spt_runtime::scrub_identity_env();\n*55|        // Anchor the monotonic half of the stderr correlation stamps at\n 56|        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n 57|        crate::conn::init_log_anchor();\n*58|        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n### deadline.rs\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 8|//!\n 9|//! Two shapes, two rules:\n 10|//!\n*11|//! - [`DeadlineAnchor`] — a **periodic** `(anchor, interval)` grid. Persisted\n*12|//!   ONCE per fresh/crash start; every fire is derived functionally\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*26|//!   BROKER-BRAIN-SPLIT-RESTORATION §7); the durable in-daemon scheduler that will\n*27|//!   drive it is the alarm-port deferral tracked in `docs/DEFERRED.md`. This\n*28|//!   helper is the *rule* the port reuses, not the *machinery*.\n 29|//!\n 30|//! **[V4] Only phase-significant loops convert.** The idempotent pump cadences\n*31|//! (`pump`) need none — they already stagger from everything-due-now,\n 32|//! catch-up/idempotent/restart-safe; converting them would re-add the per-loop\n 33|//! writes Q4 minimizes.\n 34|\n...\n 43|///\n*44|/// Keyed, not a singleton (ADR-0018 D5 amendment 6): a singleton would\n 45|/// cross-clobber the moment the live-agent adapter's **per-agent** pulse drivers\n*46|/// arrive (REQ-HAZARD-PER-AGENT-SCHEDULING / KH 7.4 — \"each agent drives its own\n 47|/// pulse\"). The daemon pulse uses key `\"pulse\"`; the adapter era passes the agent\n*48|/// id. Same code, parameterized path.\n 49|pub fn anchor_path(key: &str) -> PathBuf {\n 50|    spt_store::perch::spt_home().join(format!(\"deadline-{key}.json\"))\n 51|}\n...\n 65|impl DeadlineAnchor {\n*66|    /// Resolve a non-zero interval (doyle minor — ONE degrade rule, unit-tested):\n 67|    /// a zero `interval_ms` degrades to the config default pulse period (a `const`\n*68|    /// that is always `> 0`), so the derive can never divide by zero. The `else\n*69|    /// 1ms` floor is belt-and-braces should that default ever be made zero.\n 70|    fn effective_interval(interval_ms: u64) -> u64 {\n 71|        if interval_ms > 0 {\n 72|            return interval_ms;\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 87|    ///\n*88|    /// A corrupt file is treated as absent (degrade-to-fresh), matching\n 89|    /// `DaemonConfig::load_from` / `EffectJournal::open`. `interval_ms` is always\n 90|    /// (re)written from the caller's live config value, so a per-machine period\n 91|    /// change takes effect on the next start.\n...\n 96|        now_ms: u64,\n*97|    ) -> std::io::Result<Self> {\n 98|        let path = anchor_path(key);\n### deelevate.rs\n*1|//! De-elevation seam for daemon spawn (KNOWN-HAZARDS 5.7).\n 2|//!\n 3|//! Membership-implies-reachability made every `spt` invocation a potential\n*4|//! daemon spawner (`ensure_running`) — including the elevation-gated ones\n*5|//! (`subnet create`/`join`, REQ-SUBNET-4). A daemon spawned with the\n*6|//! elevated token is poison: on Windows its named pipes deny unelevated\n 7|//! clients (every later `spt` reads not-running → spawn → bind\n*8|//! Access-denied); on Linux a sudo'd spawn roots the daemon and — because\n 9|//! sudo flips `$HOME` — can mint a *different node identity* in root's\n 10|//! universe (the key-flip behind the 4.10 ghost rows).\n 11|//!\n*12|//! The invariant: **the daemon always runs unelevated in the invoking\n 13|//! user's universe, regardless of which command spawns it.** Two\n 14|//! enforcement points consume this module:\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 20|//! - The [`crate::daemon::Daemon::run`] entry guard catches a *directly*\n*21|//!   elevated `spt daemon` — Linux drops privileges in-process\n 22|//!   ([`SudoInvoker::drop_in_process`]) before touching any state; Windows\n*23|//!   respawns de-elevated and exits.\n 24|//!\n*25|//! When no unelevated identity exists to drop to (UAC disabled, genuine\n 26|//! root login, SYSTEM), callers run as-is with a loud warning — a\n*27|//! consistent universe, never a torn one. Elevated one-shot *clients*\n*28|//! talking downward to an unelevated daemon are fine; the daemon side is\n 29|//! the invariant.\n 30|//!\n*31|//! The OS probes mirror `spt`'s REQ-PAIR-6 `elevation.rs` (the CLI gate);\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 37|\n 38|#[cfg(unix)]\n 39|pub use unix::{daemon_target_user, sudo_invoker, SudoInvoker};\n...\n 41|#[cfg(windows)]\n*42|pub use windows::{has_deelevation_target, is_elevated, spawn_deelevated};\n 43|\n*44|/// What a spawner should do about elevation — the pure decision seam (the\n 45|/// REQ-PAIR-6 `gate_for` posture: the OS probe is environment-dependent,\n*46|/// the decision is unit-tested both ways).\n 47|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 48|pub enum SpawnPlan {\n### digest.rs\n 1|//! The session-digest **log projection** — the daemon-side, on-demand projection\n*2|//! of an endpoint's live activity buffer from its normalized session logs\n 3|//! (ADR-0008 amendment, 2026-06-12; REQ-TERM-4 log-source re-foundation).\n 4|//!\n*5|//! ## On-demand projection, not a byte feed\n 6|//!\n*7|//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n*8|//! on every output chunk. The digest is now a **projection of the same normalized\n*9|//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n 10|//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n 11|//! records via [`project_endpoint_digest`], and the hub publishes the result.\n*12|//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n 13|//! read the logs.\n 14|//!\n 15|//! ## Sources (one per endpoint)\n...\n 17|//! - An adapter that declares `[history]` (Path A locate+normalize / fetcher, or\n*18|//!   Path B native store) → the projection tails those normalized records. This\n*19|//!   is the **topology-independent** path: a Claude-Code-hosted endpoint's\n*20|//!   per-session JSONL is Path A's worked example.\n 21|//! - A **log-less** adapter pushes digest-records directly via `api digest-entry`\n 22|//!   into the perch's `digest.log` → the projection reads that.\n 23|//!\n*24|//! The records carry the published digest-record contract\n*25|//! ([`spt_term::DigestRecord`]); the projection folds them into the structured\n*26|//! [`Digest`] (N-turn window, collapsed tool sprints, ~25-char arg truncation).\n 27|//!\n*28|//! ## Snapshot vs delta\n 29|//!\n*30|//! A projection is diffed against the hub's prior snapshot\n 31|//! ([`crate::digesthub::DigestHub::project_and_publish`]) to produce a\n*32|//! [`DigestUpdate`]: `from` is where the changed turns begin, and the subscriber\n 33|//! applies it by **truncating its view to `from`, then appending `turns`**. The\n*34|//! delta-stream **contract** ships; its deltas are driven by pull / digest-entry\n*35|//! publishes (autonomous file-watch freshness is deferred to the consuming\n 36|//! frontend milestone — CONTEXT.md \"session digest\").\n 37|// [impl->REQ-TERM-4]\n 38|\n...\n 45|use spt_runtime::manifest::Digest as ManifestDigest;\n*46|use spt_store::perch::{self, ParentHint};\n*47|use spt_term::{Digest, DigestConfig, TimelineItem, Turn};\n 48|\n 49|/// Bound for the projection's extractor run (the `[digest]` extractor is a\n*50|/// subprocess — REQ-HAZARD-SUBPROCESS-TIMEOUT; a hang is killed, never blocks).\n 51|const DIGEST_FETCH_TIMEOUT: Duration = Duration::from_secs(30);\n 52|\n### digesthub.rs\n*1|//! The daemon-held digest hub + its control channel (ADR-0008 amendment /\n 2|//! REQ-TERM-4).\n 3|//!\n*4|//! The hub holds the latest projected snapshot per endpoint and fans every change\n*5|//! out to live subscribers. The digest is **projected on demand** from the\n*6|//! endpoint's normalized records ([`project_endpoint_digest`]) — there is no live\n*7|//! feed; a pull or a `digest-entry` push triggers a projection, and\n 8|//! [`DigestHub::project_and_publish`] diffs it against the prior snapshot to\n*9|//! produce the [`DigestUpdate`] delta. The control channel is the access surface\n 10|//! ADR-0008 describes — two modes over one transport:\n 11|//!\n\n[Showing lines 1-899 of 989 (50.0KB limit). Use :900 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":400,"fileCount":20,"files":["crates/spt-daemon/Cargo.toml","crates/spt-daemon/src/access.rs","crates/spt-daemon/src/adapter_update.rs","crates/spt-daemon/src/applyhost.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/autostart.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/codec.rs","crates/spt-daemon/src/config.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/consent.rs","crates/spt-daemon/src/crc_swap.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/digesthub.rs"],"fileMatches":[{"path":"crates/spt-daemon/Cargo.toml","count":20},{"path":"crates/spt-daemon/src/access.rs","count":20},{"path":"crates/spt-daemon/src/adapter_update.rs","count":20},{"path":"crates/spt-daemon/src/applyhost.rs","count":20},{"path":"crates/spt-daemon/src/attach.rs","count":20},{"path":"crates/spt-daemon/src/autostart.rs","count":20},{"path":"crates/spt-daemon/src/bin/xlate_choreo_fixture.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/codec.rs","count":20},{"path":"crates/spt-daemon/src/config.rs","count":20},{"path":"crates/spt-daemon/src/conn.rs","count":20},{"path":"crates/spt-daemon/src/consent.rs","count":20},{"path":"crates/spt-daemon/src/crc_swap.rs","count":20},{"path":"crates/spt-daemon/src/daemon.rs","count":20},{"path":"crates/spt-daemon/src/deadline.rs","count":20},{"path":"crates/spt-daemon/src/deelevate.rs","count":20},{"path":"crates/spt-daemon/src/digest.rs","count":20},{"path":"crates/spt-daemon/src/digesthub.rs","count":20}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/\n## Cargo.toml\n  3│version.workspace = true\n *4│edition = \"2021\"\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│# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for\n *9│# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /\n 10│# REQ-STREAM-OPENER-DURABLE / REQ-DISPATCH-CLAIM-RETRY): the REAL\n*11│# `run_dispatch_loop` as a KILLABLE child process — a killed generation drops\n 12│# its workers' broker conns exactly like a production brain death. Same\n*13│# tests/-homed `[[bin]]` pattern as spt's fixtures (excluded from the release\n 14│# build, which is `--bin spt` only). Name avoids the substring \"update\"\n 15│# (the Windows installer-detection UAC-740 class).\n 16│[[bin]]\n   │...\n 20│[dependencies]\n*21│# Layer-below set (R-ARCH-1 acyclic): …→spt-live→spt-daemon→spt. The broker\n*22│# re-hosts spt-term PTYs + the spt-live lifecycle seams; these edges fix the\n 23│# crate's position in the workspace layering. Tasks B1–B8 consume them.\n 24│spt-proto = { path = \"../spt-proto\" }\n 25│spt-store = { path = \"../spt-store\" }\n   │...\n 34│spt-net = { path = \"../spt-net\" }\n*35│# The broker is otherwise sync/thread-based; iroh requires tokio, so the broker\n*36│# hosts a dedicated runtime on named worker threads (QUIC objects never leave it).\n 37│tokio = { version = \"1\", features = [\"rt-multi-thread\", \"net\"] }\n 38│# The node-local docs server (ADR-0036 §4, REQ-DOCS-LOCAL-SERVER): hyper +\n*39│# its util crates are ALREADY in the lock via iroh/reqwest — battle-tested\n*40│# crate over hand-rolled HTTP (operator-ruled), zero new supply chain.\n 41│hyper = { version = \"1\", features = [\"server\", \"http1\"] }\n 42│hyper-util = { version = \"0.1\", features = [\"tokio\"] }\n 43│http-body-util = \"0.1\"\n   │...\n 45│serde_json = { version = \"1\", features = [\"preserve_order\"] }\n*46│# PTY output is arbitrary bytes; the forward-compat JSON envelope carries it as\n 47│# a base64 string in the payload rather than forking the codec for binary.\n 48│base64 = \"0.22\"\n*49│# Cross-platform local IPC: named pipe (Windows) / Unix-domain socket (unix)\n 50│# behind one API — the OS-split substrate the DaemonTransport trait wraps,\n 51│# mirroring how spt-term abstracts ConPTY vs forkpty behind SessionSurface.\n 52│interprocess = \"2\"\n*53│# Self-update release signing (C1, REQ-UPD-2): pure-Rust Ed25519 — no C/asm\n 54│# build deps, clean cross-compile on win+linux (clean-room posture, ADR-0004 §D).\n*55│# `rand_core` lets a release-tooling caller mint a keypair; tests use the\n 56│# deterministic `from_bytes` ctor (no RNG).\n*57│ed25519-dalek = \"2\"\n*58│# SHA-256 binds the signed release metadata to the actual binary bytes\n 59│# (artifact digest) so the signature covers the binary, not just the manifest.\n 60│sha2 = \"0.10\"\n*61│# Per-frame link auth on the same-node shell channels (CONTEXT local-link\n*62│# authentication): HMAC-SHA256 over the token-derived link key (M5-D3b).\n 63│hmac = \"0.12\"\n 64│\n\n## src/\n### access.rs\n  2│//!\n *3│//! ADR-0009 models access as **three orthogonal, nested gates**, evaluated in\n  4│//! order: subnet *visibility* (is the endpoint routable here at all? —\n *5│//! `spt_store::visibility`, applied inside registry resolution) → the\n *6│//! **endpoint access whitelist** (may *your node* remotely reach it? — THIS\n  7│//! module) → *capability grants* (may this agent do this high-risk thing? —\n *8│//! [`crate::consent`], default-deny). The whitelist is the outer gate before\n *9│//! grants: an origin the whitelist refuses never reaches a grant check.\n 10│//!\n*11│//! ## Whitelist semantics (stateful firewall, not a blanket block)\n 12│//!\n 13│//! - **Origin-node gate:** the subject is the node the inbound interaction\n 14│//!   *originates from* (where the operator is acting) — never the sender\n   │...\n 16│//!   handshake-proven remote node id from the broker's conn/stream table,\n*17│//!   never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).\n*18│//! - **Outbound** → any visible node: the whitelist never restricts who an\n 19│//!   endpoint talks *to*.\n*20│//! - **Inbound reply** correlated to the endpoint's own prior outbound\n*21│//!   (keyed on the inbound `from`) → allowed from any visible node\n*22│//!   (\"established/related\").\n 23│//!   Classification reads the durable [`RecentOutbound`] log\n*24│//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].\n*25│//! - **Unsolicited/direct inbound** → only from a whitelisted node.\n*26│//! - **Same-node operation is always allowed** — the gate sits on the\n*27│//!   net-inbound path only, so the local delivery path never sees it; an\n*28│//!   origin that *is* this node (a loopback self-dial) short-circuits Allow.\n*29│//! - **Default absent = open**; setting a whitelist restricts (an empty entry\n*30│//!   = locked down). Node-tier ships M4 (Ed25519 node pubkeys); the user tier\n*31│//!   is an inert schema reservation until the cross-user model (ADR-0006).\n 32│//!\n 33│//! The store is [`spt_store::access::AccessStore`] (`trust/access.json` —\n 34│//! security material next to the trust store). The gate re-reads it per check:\n*35│//! an `spt access` edit takes effect on the next inbound without a daemon\n 36│//! restart, and the checks run at message/stream-establishment rate where a\n 37│//! small-file read is noise.\n 38│//!\n### adapter_update.rs\n *1│//! Adapter ripple-update conduct (C3) — spt-core updates the whole stack: self\n *2│//! first, then each registered adapter via its manifest `[update]` avenue, with\n  3│//! **adapter content signing** (REQ-UPD-5, REQ-SEAM-UPDATE, ADR-0004 §D).\n  4│//!\n  5│//! After spt-core's own update completes (the C0–C2 engine), it conducts a\n *6│//! ripple over its registered adapters. Each adapter declares how it updates in\n  7│//! its manifest `[update]` block ([`Update`]); this module turns that directive\n *8│//! into a per-adapter [`AdapterUpdateOutcome`], dispatched by avenue:\n  9│//!\n 10│//! - **`file_pull`** — spt-core controls the bytes, so it *verifies* them: the\n*11│//!   pulled payload is checked against the adapter's **own** Ed25519 key (declared\n*12│//!   in the manifest — the adapter author signs their releases; spt-core's release\n*13│//!   key stays scoped to spt-core) plus the SHA-256 artifact binding. Authentic +\n*14│//!   intact ⇒ [`AdapterUpdateOutcome::Verified`]; otherwise\n*15│//!   [`AdapterUpdateOutcome::Rejected`].\n*16│//! - **`delegated`** — the adapter's own updater installs the content (e.g.\n 17│//!   `claude.exe plugin update`); spt-core never sees those bytes, so there is\n*18│//!   nothing for it to hash. It delegates the trust **only** when the adapter\n*19│//!   attests `self_verifies` (its updater checks its own signatures) ⇒\n*20│//!   [`AdapterUpdateOutcome::Delegate`]; an unattested delegated update is\n*21│//!   [`AdapterUpdateOutcome::Skipped`] as unverifiable.\n 22│//!\n 23│//! The actual byte transport for `file_pull` (the network pull) is M4\n*24│//! (`REQ-UPD-1`); v1 receives the payload via self-fetch / out-of-band and feeds\n 25│//! it here. This module is the **decision + verification** layer — it does not\n*26│//! itself fetch or exec.\n 27│// [impl->REQ-UPD-5]\n 28│// [impl->REQ-SEAM-UPDATE]\n 29│\n   │...\n 31│\n*32│use crate::release::{\n*33│    parse_verifying_key, verify_artifact, verify_signature, RejectReason, ReleaseMetadata,\n*34│    SignedRelease,\n 35│};\n 36│\n*37│/// A pulled `file_pull` payload awaiting verification: the adapter's signed\n### applyhost.rs\n *1│//! Staged-update apply orchestration (M5-D7b) — the **production caller** for\n  2│//! [`crate::update::apply_brain_only`], closing the M4-D9 honest residual\n  3│//! (\"nothing in the daemon invokes this apply after the user acks the consent\n  4│//! notif\").\n   │...\n  9│//! notif and is a *prompt* answer, not a lock — an apply after a dismiss\n*10│//! still works, the user changed their mind).\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*16│//!    `release-keys.json` policy): apply never trusts the cache — a tampered\n*17│//!    or expired artifact refuses here. The monotonic-version floor was\n*18│//!    enforced at *stage* time (REQ-HAZARD-UPDATE-ROLLBACK); the apply-time\n 19│//!    re-check guards authenticity/integrity/expiry of the bytes on disk.\n 20│//! 2. **Classify** against the running broker's ABI. Only\n 21│//!    [`UpdateClass::BrainOnly`] applies in M5 — the broker-touching classes\n*22│//!    refuse typed, the artifact stays staged (their consent-and-scheduling\n 23│//!    orchestration is post-M5; ADR-0004 §A).\n 24│//! 3. **Swap the binary**: the current executable steps aside\n 25│//!    (`<exe>.old-<ver>` — a running image survives its own rename on both\n*26│//!    OSes) and the verified artifact lands at the executable path\n*27│//!    atomically. A failed landing rolls the original back — never a\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 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 41│// [impl->REQ-UPD-4]\n   │...\n 47│use crate::brain::{now_ms, Brain};\n*48│use crate::relcache::{AppliedRecord, ReleaseCache, StagedUpdate};\n*49│use crate::release::{current_platform, VerifyPolicy};\n*50│use crate::update::{plan_verified, plan_verified_update_set, BrokerAbi, UpdateClass};\n 51│\n*52│/// What [`apply_staged`] did — loud + typed, never silent.\n 53│#[derive(Debug, Clone, PartialEq, Eq)]\n### attach.rs\n  3│//!\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 *9│//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n 10│//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n*11│//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n*12│//!   journal (a replayed input record cannot double-type);\n 13│//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n 14│//!\n*15│//! ## Exactly-once lives at the EDGES, not the wire sends\n 16│//!\n*17│//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\n*18│//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n*19│//! and *discards* everything interleaved — inside a full-duplex pump loop that\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*25│//! re-transmit output records on the wire — duplicates are dropped at the\n*26│//! render cursor, the same discipline the local `OutputLog` replay uses\n 27│//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n 28│//!\n 29│//! **Compute + files stay here**; the operator is a viewport (CONTEXT\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*35│//! the D5d whitelist drops in at this same call.\n 36│//!\n 37│//! Restart survival composes from D4/B5 with **no new machinery**: the broker\n 38│//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n*39│//! A restarted target brain re-attaches the session from its output cursor,\n*40│//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n*41│//! a restarted operator brain resubscribes and dedups rendered chunks by\n 42│//! `Output.seq`.\n 43│\n 44│use std::io;\n### autostart.rs\n  4│//! always-there, but a box reboot / daemon cold start left it down until\n *5│//! hands-on (deployah field-confirm 2026-07-10, post-v0.30.6 restart). The\n  6│//! operator marks an endpoint a **startup default** with `spt endpoint run\n *7│//! --save` (the endpoint analog of the shipped `subnet attach --save`\n *8│//! precedent — doyle-ruled shape (a)); the daemon replays the saved runs here\n  9│//! at start.\n 10│//!\n 11│//! Contract: a replay is best-effort and LOUD — an unresolvable adapter or a\n*12│//! failed launch logs its skip and moves on; nothing here can block daemon\n 13│//! start or the other replays. Each replay is a FRESH session (shape (c)\n*14│//! restore-what-was-up was rejected): the adapter option re-resolves against\n*15│//! the live registry, the session id is freshly minted, and home rides the\n 16│//! endpoint's existing perch (durable — REQ-INST-15), so no `--subnet` is\n*17│//! recorded or needed.\n 18│\n 19│use crate::config::DaemonConfig;\n 20│\n*21│/// How long to keep retrying a launch whose broker round-trip failed — the\n 22│/// replay thread starts alongside the broker's serve thread, so the very first\n*23│/// connect can race the accept loop up. Bounded and short: a genuine launch\n 24│/// failure (dead adapter binary, bad template) fails again instantly and we\n 25│/// move on after the last attempt.\n 26│const LAUNCH_ATTEMPTS: u32 = 3;\n*27│const LAUNCH_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(750);\n 28│\n*29│/// Replay every saved startup default against the (just-bound) broker. Runs on\n 30│/// a daemon-boot background thread — never returns an error, never panics the\n 31│/// daemon; every outcome is a loud stderr token:\n*32│/// - `ENDPOINT_AUTOSTART:<id>` — the saved run came up;\n*33│/// - `ENDPOINT_AUTOSTART_SKIP:<id>` — the saved adapter no longer resolves to a\n*34│///   registered harness (the save is stale; re-run `endpoint run --save`);\n*35│/// - `ENDPOINT_AUTOSTART_FAIL:<id>` — the launch itself failed after retries.\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   │...\n 43│    // `{node}` fill: the same OS-hostname fallback the CLI spawn path uses —\n*44│    // the in-mem label handle lives daemon-side but this thread predates it.\n*45│    let node_label = spt_store::hostlabel::node_fill_label(None);\n 46│    for entry in entries {\n*47│        // Re-resolve the adapter option fresh (an adapter updated between\n*48│        // restarts is honored; an uninstalled one is a loud skip, never a wedge).\n 49│        let (record, manifest) =\n 50│            match spt_runtime::registry::resolve_option(&adapters_dir, &entry.adapter) {\n 51│                Ok((r, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => {\n   │...\n 55│                    eprintln!(\n*56│                        \"ENDPOINT_AUTOSTART_SKIP:{}: saved adapter '{}' is not a harness \\\n 57│                         adapter — re-save with `spt endpoint run --save`\",\n 58│                        entry.id, entry.adapter\n 59│                    );\n### brain.rs\n  2│//!\n *3│//! The brain holds all the volatile logic; it may be killed and restarted at\n *4│//! any moment (a routine self-update swaps it; a crash kills it). It connects to\n *5│//! the stable [`crate::broker::Broker`] over the versioned IPC, drives sessions\n  6│//! (spawn / input / resize / kill), and consumes their output. Two invariants\n  7│//! this type encodes:\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 21│//!   error — so a lost chunk can never pass silently.\n 22│// [impl->REQ-HAZARD-GEN-START-NOW]\n   │...\n 25│use std::io;\n*26│use std::sync::mpsc::{self, Receiver, RecvTimeoutError};\n*27│use std::thread::{self, JoinHandle};\n 28│use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n 29│\n*30│use interprocess::local_socket::prelude::*;\n 31│\n 32│use interprocess::local_socket::{SendHalf, Stream};\n 33│\n 34│use crate::codec::{read_frame, write_frame};\n*35│use crate::frame::{Envelope, Role};\n*36│use crate::effect::MintedOp;\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*42│    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n*43│    NetStreamSubscribeReq, NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, NetStreamUnsubscribeReq, NetStreamUnsubscribed, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n*44│    SessionsReply, SizeEvent, SpawnConflict, SpawnReq, Spawned, StreamLifetime, SubscribeOutcome,\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 *5│//! drifted to an in-process thread (`daemon.rs`), so `spt update apply` swaps\n  6│//! the binary on disk but never restarts the running code (the regression\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 12│//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n*13│//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n*14│//! verb. The digest hub serve + seed-control stay broker-side continuity\n 15│//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n 16│//! loops.\n 17│//!\n   │...\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 25│use std::io;\n   │...\n 28│use std::sync::atomic::{AtomicBool, Ordering};\n*29│use std::sync::{Arc, OnceLock};\n 30│use std::thread;\n 31│use std::time::{Duration, Instant};\n 32│\n   │...\n 34│use crate::endpoint::{brain_ready_path, broker_socket_name};\n*35│use crate::relcache::{AppliedRecord, ReleaseCache};\n 36│\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 42│/// never a hot respawn loop.\n 43│const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n 44│/// A run at least this long counts as healthy and resets the backoff floor.\n*45│/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n 46│/// brain has this long to signal `ready` for its generation before the window\n*47│/// elapses (the \"up but useless\" alive-never-ready rollback path).\n 48│const SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\n 49│/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\n*50│/// **exits before** signaling ready is respawned this many consecutive times\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 56│/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\n   │...\n 59│/// How long the brain child retries connecting to the broker socket before\n*60│/// giving up (the broker may still be binding — the child is freshly spawned).\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### broker.rs\n *1│//! The broker kernel (B1 + B2) — the stable half of the broker/brain split.\n  2│//!\n  3│//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n *4│//! would lose continuity on if the logic restarted: the PTY master+writer and\n *5│//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n  6│//! client / listening sockets. It runs **no logic** — no routing, registry,\n *7│//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n  8│//! forward its output to the brain, inject the brain's input, report exit.\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 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*18│//!   [`OutputLog`]: every chunk gets a monotonic per-session `seq` and is held\n*19│//!   in a bounded ring. The current subscriber (a brain connection) receives\n 20│//!   chunks live; all subscriber writes happen *under the log lock*, so frames\n 21│//!   reach the brain in strict `seq` order with no interleave.\n*22│//! - **Resume from a cursor.** A reconnecting brain sends `subscribe` with its\n 23│//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\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 29│//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n 30│//! output log persist. A session leaves the table only when its child exits\n*31│//! (reaped by the exit waiter) or is explicitly killed.\n 32│// [impl->REQ-DAEMON-2]\n 33│\n 34│use std::collections::{HashMap, HashSet, VecDeque};\n   │...\n 36│use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};\n*37│use std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};\n*38│use std::sync::{Arc, Mutex, OnceLock};\n*39│use std::thread::{self, JoinHandle};\n 40│use std::time::{Duration, Instant};\n 41│\n*42│use interprocess::local_socket::{prelude::*, Stream};\n 43│use spt_term::{Drain, PtySession, ScreenGrid, SessionSurface, SurfaceSize};\n 44│\n 45│use spt_net::net::attach::AttachIntent;\n   │...\n 51│use crate::effect::{EffectJournal, EffectKey, EffectKind, Minter, Outcome};\n*52│use crate::frame::{Envelope, Role};\n 53│use crate::msg::{\n*54│    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,\n### codec.rs\n *1│//! Length-prefixed framing for the broker↔brain IPC.\n  2│//!\n  3│//! Frame: a 4-byte big-endian length prefix, then that many bytes of UTF-8\n *4│//! JSON encoding one [`Envelope`]. Same shape as `spt-msg::wire` (ADR-0001\n  5│//! framing) but generic over any [`Read`]/[`Write`] — the IPC carrier is a\n *6│//! local socket (named pipe / UDS), not a `TcpStream`, and tests exercise it\n  7│//! over in-memory buffers and socket pairs.\n  8│//!\n *9│//! The frame *codec* is deliberately transport-agnostic: it is the byte-level\n 10│//! contract, while [`crate::transport`] owns the OS-specific connection.\n 11│// [impl->REQ-DAEMON-2]\n 12│\n*13│use std::io::{self, Read, Write};\n 14│\n*15│use crate::frame::Envelope;\n 16│\n*17│/// Maximum framed message size accepted (16 MiB) — guards a garbage/hostile\n 18│/// length prefix from forcing a huge allocation. Matches `spt-msg::wire`.\n 19│const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;\n 20│\n*21│/// Serialize and write one [`Envelope`] as a length-prefixed JSON frame.\n*22│pub fn write_frame<W: Write>(w: &mut W, env: &Envelope) -> io::Result<()> {\n 23│    let body =\n 24│        serde_json::to_vec(env).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 25│    if body.len() > MAX_FRAME_BYTES {\n   │...\n 37│\n*38│/// Read one length-prefixed JSON frame and decode it into an [`Envelope`].\n*39│/// Rejects an oversized length prefix before allocating. A clean EOF on the\n*40│/// length prefix surfaces as [`io::ErrorKind::UnexpectedEof`] (peer hung up).\n*41│pub fn read_frame<R: Read>(r: &mut R) -> io::Result<Envelope> {\n 42│    let mut len_buf = [0u8; 4];\n 43│    r.read_exact(&mut len_buf)?;\n 44│    let len = u32::from_be_bytes(len_buf) as usize;\n   │...\n 64│    fn frame_round_trips_through_buffer() {\n*65│        let env = Envelope::new(\"attach\", json!({ \"session\": \"s1\", \"n\": 7 }));\n 66│        let mut buf: Vec<u8> = Vec::new();\n 67│        write_frame(&mut buf, &env).unwrap();\n 68│\n*69│        let mut cursor = io::Cursor::new(buf);\n*70│        let got = read_frame(&mut cursor).unwrap();\n 71│        assert_eq!(got.protocol_version, IPC_PROTOCOL_VERSION);\n 72│        assert_eq!(got.kind, \"attach\");\n 73│        assert_eq!(got.payload, json!({ \"session\": \"s1\", \"n\": 7 }));\n   │...\n 75│\n*76│    // [unit->REQ-DAEMON-2] multiple frames stream back-to-back without bleed.\n 77│    #[test]\n 78│    fn back_to_back_frames_decode_independently() {\n 79│        let mut buf: Vec<u8> = Vec::new();\n*80│        write_frame(&mut buf, &Envelope::new(\"a\", json!(1))).unwrap();\n*81│        write_frame(&mut buf, &Envelope::new(\"b\", json!(2))).unwrap();\n 82│\n*83│        let mut cursor = io::Cursor::new(buf);\n### config.rs\n  3│//!\n *4│//! In the no-daemon interim model the pulse cadence was a hard-coded\n  5│//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\n *6│//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\n *7│//! period is **durable and configurable**, sourced from a small JSON file under\n  8│//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\n *9│//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing\n*10│//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence\n 11│//! from config, never a constant.\n 12│// [impl->REQ-DAEMON-1]\n 13│\n   │...\n 18│\n*19│/// The interim default pulse period carried forward as the config default. The\n 20│/// daemon now treats this as a *fallback*, not a constant: `daemon.json` may\n 21│/// override it per machine.\n 22│pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\n 23│\n*24│/// Default drop-file precedence protection window (epoch-ms), mirroring the\n*25│/// interim `spt_live` guard. Surfaced here so the one config source owns it.\n 26│pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;\n 27│\n*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 35│/// Default peer-pump cadences (D9-1): how often the daemon pushes its\n*36│/// registry/notif feeds to trusted peers and pulls context syncs from them.\n 37│/// Conservative defaults — every loop is a thin call over an idempotent\n*38│/// driver, so a missed tick costs only latency, never correctness.\n 39│pub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\n 40│pub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\n 41│pub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\n   │...\n 44│/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\n*45│/// authored by a node not heard within this window are evicted on the next\n*46│/// registry pump tick. Sized to ~10 default pump cadences — generous against\n*47│/// transient drops (a peer re-advertises every cadence, so one admitted feed\n 48│/// inside the window keeps its rows), short enough that a dead node identity\n 49│/// stops poisoning bare-id resolution within minutes.\n 50│pub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\n 51│\n*52│/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —\n*53│/// the `\"relay\"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in\n 54│/// `Daemon::run`; kept as a local enum so the config layer stays transport-free.\n*55│/// The self-hosted variant (`RelayPolicy::SelfHosted`) needs a relay-map URL\n### conn.rs\n *1│//! Bounded, cancelable, poison-on-failure framed writes for one physical\n *2│//! broker connection (UPDATE-WEDGE round-4).\n  3│//!\n  4│//! ## The failure class this kills\n  5│//!\n *6│//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\n  7│//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\n  8│//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n *9│//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n*10│//! A connected consumer that stops reading (an `rc --take` whose process\n*11│//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n*12│//! logical stall-evict (`stall_evict_controller`) releases only the controller\n*13│//! ROLE — it never cancels the in-flight pipe write or closes the physical\n*14│//! connection, so the writer (and every write queued behind the same gate)\n*15│//! stays wedged until the peer process dies (~128 s in the field capture, on a\n 16│//! brain restart).\n 17│//!\n 18│//! ## The primitive\n   │...\n 20│//! [`BrokerConn`] owns the send half outright — it never leaves this module,\n*21│//! so an unbounded bypass write cannot compile. Every write:\n 22│//!\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   │...\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### consent.rs\n *1│//! Self-update consent gating (C2) — the REQ-UPD-4 decision + the\n  2│//! most-recently-active-session resolver.\n  3│//!\n *4│//! ADR-0004 makes self-update **not fully automatic by default**: a verified,\n *5│//! eligible update is gated on a user confirmation delivered to the user's\n  6│//! **most-recently-active live session**, with an explicit opt-in to full-auto\n  7│//! (the seamless, prompt-free path). C2 ships the load-bearing, fully-testable\n  8│//! core of that:\n  9│//!\n*10│//! - [`decide`] — the **pure** gate: full-auto ⇒ [`ConsentDecision::AutoApproved`];\n*11│//!   otherwise [`ConsentDecision::NeedsConsent`] carrying the resolved target the\n 12│//!   orchestrator should prompt.\n 13│//! - [`most_recently_active`] — resolve that target over the **v1 registry**\n 14│//!   (live perches + their `last_active_ms` recency stamp). This is the\n*15│//!   registry-resolution precursor CONTEXT names; the deferred PresenceChannel\n 16│//!   later generalizes it.\n 17│//!\n*18│//! What C2 deliberately does **not** build: the interactive prompt UX itself.\n*19│//! That rides the deferred notification primitive (ADR-0007 — `update-available`\n*20│//! / `consent-needed` producers) and the harness's native confirm affordance\n*21│//! (e.g. AskUserQuestion). [`ConsentDecision::NeedsConsent`] is the typed seam\n 22│//! those plug into; nothing here reaches into a session.\n 23│// [impl->REQ-UPD-4]\n 24│\n   │...\n 26│\n*27│/// The session a consent prompt should be delivered to: the user's\n 28│/// most-recently-active live perch. Carries enough to address it (id + path) and\n*29│/// the recency that won it the selection (for logging / tie-break audit).\n 30│#[derive(Debug, Clone, PartialEq, Eq)]\n 31│pub struct ConsentTarget {\n 32│    /// The agent/perch id of the most-recently-active live session.\n 33│    pub id: String,\n*34│    /// Its resolved perch directory.\n 35│    pub perch_path: PathBuf,\n*36│    /// Its `last_active_ms` recency stamp (0 if never stamped — least-recent).\n 37│    pub last_active_ms: u64,\n 38│}\n 39│\n*40│/// The gate's verdict for a verified, eligible update (REQ-UPD-4).\n 41│#[derive(Debug, Clone, PartialEq, Eq)]\n 42│pub enum ConsentDecision {\n*43│    /// Full-auto is opted in — apply without a prompt (the seamless path).\n*44│    AutoApproved,\n*45│    /// Default-gated — hold the update until the user confirms. Carries the\n*46│    /// resolved session to prompt, or `None` when no live session could be\n*47│    /// resolved (the orchestrator then defers / falls back to a node-level surface\n### crc_swap.rs\n *1│//! W3b — content-hash-gated atomic per-file swap for a live adapter update\n  2│//! (ADR-0025, REQ-ADAPTER-LIVE-UPDATE).\n  3│//!\n  4│//! The legacy `spt adapter update` path does `remove_dir_all(install_dir)` then a\n *5│//! blanket extract — which fails on Windows with *\"Access is denied (os error 5)\"*\n *6│//! the moment a single install-dir binary is still running (the field bug). This\n *7│//! module replaces that with a **content-diff swap**: extract the staged archive\n  8│//! to a temp tree, replace in the install dir **only** the files whose content\n  9│//! hash differs (so identical files — and their still-running binaries — are\n*10│//! never touched), and commit via **stage-all-`.new`-then-one-tight-rename-loop**\n 11│//! so a mid-swap failure never strands the install half-new (doyle W3 gate,\n 12│//! binding partial-failure refinement).\n 13│//!\n*14│//! CRC here is plain content hashing (reuse [`crate::release::sha256_hex`], no new\n*15│//! dep) — \"replace only what changed\", not cryptographic integrity (that is the\n 16│//! archive signature, ADR-0024 / REQ-UPD-9).\n 17│\n*18│use crate::release::sha256_hex;\n 19│use std::path::{Path, PathBuf};\n 20│\n*21│/// One planned file replacement discovered by [`plan_crc_swap`].\n 22│#[derive(Debug, Clone, PartialEq, Eq)]\n*23│pub struct PlannedSwap {\n*24│    /// Path relative to the tree root (for diagnostics / determinism).\n*25│    pub rel: PathBuf,\n*26│    /// The staged source file (`<staging>/<rel>`).\n*27│    pub staged: PathBuf,\n*28│    /// The install-dir target (`<install_dir>/<rel>`).\n 29│    pub target: PathBuf,\n 30│}\n 31│\n   │...\n 33│/// `install_dir` counterpart — or that are absent in `install_dir`. Files whose\n*34│/// content is byte-identical are skipped, so a still-running unchanged binary is\n*35│/// never disturbed. Read-only: does not mutate either tree. The returned plan is\n*36│/// sorted by `rel` for deterministic apply + testing.\n 37│// [impl->REQ-ADAPTER-LIVE-UPDATE]\n*38│pub fn plan_crc_swap(staging: &Path, install_dir: &Path) -> std::io::Result<Vec<PlannedSwap>> {\n 39│    let mut plan = Vec::new();\n 40│    collect_diffs(staging, staging, install_dir, &mut plan)?;\n*41│    plan.sort_by(|a, b| a.rel.cmp(&b.rel));\n 42│    Ok(plan)\n 43│}\n 44│\n### daemon.rs\n  3│//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds\n *4│//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the\n  5│//! canonical per-home names ([`crate::endpoint`]) and serves both for the life\n  6│//! of the process.\n  7│//!\n  8│//! [`ensure_running`] is the auto-start seam (REQ-DAEMON-3): any `api`\n *9│//! invocation calls it; if no daemon answers the seed-control ping it spawns\n*10│//! `spt daemon` detached and waits (bounded) for it to come up. The reliable\n*11│//! anchor is the seed channel — bound for the whole daemon lifetime, so a ping\n*12│//! against it is a true liveness test (`listen` is the interim model's anchor;\n*13│//! here the seed channel plays that role for cold-start detection).\n 14│// [impl->REQ-DAEMON-3]\n 15│\n 16│use std::io;\n   │...\n 22│\n*23│use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n 24│\n 25│use crate::broker::{default_journal_path, Broker};\n*26│use crate::config::{DaemonConfig, RelayChoice};\n*27│use crate::digesthub::{self, DigestHub};\n 28│use crate::endpoint::{\n*29│    broker_socket_name, daemon_pid_path, digest_socket_name, drive_socket_name, seed_socket_name,\n 30│};\n 31│use crate::nethost::{NetConfig, NetHost};\n*32│use crate::seedmap::{self, SeedRegistry};\n 33│\n*34│/// How long auto-start waits for a freshly-spawned daemon to bind its channel.\n 35│const START_TIMEOUT: Duration = Duration::from_secs(5);\n*36│/// Poll cadence while waiting for the spawned daemon to come up.\n 37│const START_POLL: Duration = Duration::from_millis(25);\n 38│\n 39│/// The per-machine broker/brain daemon process.\n   │...\n 42│impl Daemon {\n*43│    /// Run the daemon: bind the broker (served on a background thread) + the\n*44│    /// seed-control channel (the foreground loop + liveness anchor) and serve\n*45│    /// until the process is killed. The body of `spt daemon`.\n 46│    pub fn run() -> io::Result<()> {\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*52│        // is touched or any child can be spawned.\n 53│        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n 54│        spt_runtime::scrub_identity_env();\n*55│        // Anchor the monotonic half of the stderr correlation stamps at\n 56│        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n 57│        crate::conn::init_log_anchor();\n*58│        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n### deadline.rs\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  8│//!\n  9│//! Two shapes, two rules:\n 10│//!\n*11│//! - [`DeadlineAnchor`] — a **periodic** `(anchor, interval)` grid. Persisted\n*12│//!   ONCE per fresh/crash start; every fire is derived functionally\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*26│//!   BROKER-BRAIN-SPLIT-RESTORATION §7); the durable in-daemon scheduler that will\n*27│//!   drive it is the alarm-port deferral tracked in `docs/DEFERRED.md`. This\n*28│//!   helper is the *rule* the port reuses, not the *machinery*.\n 29│//!\n 30│//! **[V4] Only phase-significant loops convert.** The idempotent pump cadences\n*31│//! (`pump`) need none — they already stagger from everything-due-now,\n 32│//! catch-up/idempotent/restart-safe; converting them would re-add the per-loop\n 33│//! writes Q4 minimizes.\n 34│\n   │...\n 43│///\n*44│/// Keyed, not a singleton (ADR-0018 D5 amendment 6): a singleton would\n 45│/// cross-clobber the moment the live-agent adapter's **per-agent** pulse drivers\n*46│/// arrive (REQ-HAZARD-PER-AGENT-SCHEDULING / KH 7.4 — \"each agent drives its own\n 47│/// pulse\"). The daemon pulse uses key `\"pulse\"`; the adapter era passes the agent\n*48│/// id. Same code, parameterized path.\n 49│pub fn anchor_path(key: &str) -> PathBuf {\n 50│    spt_store::perch::spt_home().join(format!(\"deadline-{key}.json\"))\n 51│}\n   │...\n 65│impl DeadlineAnchor {\n*66│    /// Resolve a non-zero interval (doyle minor — ONE degrade rule, unit-tested):\n 67│    /// a zero `interval_ms` degrades to the config default pulse period (a `const`\n*68│    /// that is always `> 0`), so the derive can never divide by zero. The `else\n*69│    /// 1ms` floor is belt-and-braces should that default ever be made zero.\n 70│    fn effective_interval(interval_ms: u64) -> u64 {\n 71│        if interval_ms > 0 {\n 72│            return interval_ms;\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 87│    ///\n*88│    /// A corrupt file is treated as absent (degrade-to-fresh), matching\n 89│    /// `DaemonConfig::load_from` / `EffectJournal::open`. `interval_ms` is always\n 90│    /// (re)written from the caller's live config value, so a per-machine period\n 91│    /// change takes effect on the next start.\n   │...\n 96│        now_ms: u64,\n*97│    ) -> std::io::Result<Self> {\n 98│        let path = anchor_path(key);\n### deelevate.rs\n *1│//! De-elevation seam for daemon spawn (KNOWN-HAZARDS 5.7).\n  2│//!\n  3│//! Membership-implies-reachability made every `spt` invocation a potential\n *4│//! daemon spawner (`ensure_running`) — including the elevation-gated ones\n *5│//! (`subnet create`/`join`, REQ-SUBNET-4). A daemon spawned with the\n *6│//! elevated token is poison: on Windows its named pipes deny unelevated\n  7│//! clients (every later `spt` reads not-running → spawn → bind\n *8│//! Access-denied); on Linux a sudo'd spawn roots the daemon and — because\n  9│//! sudo flips `$HOME` — can mint a *different node identity* in root's\n 10│//! universe (the key-flip behind the 4.10 ghost rows).\n 11│//!\n*12│//! The invariant: **the daemon always runs unelevated in the invoking\n 13│//! user's universe, regardless of which command spawns it.** Two\n 14│//! enforcement points consume this module:\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 20│//! - The [`crate::daemon::Daemon::run`] entry guard catches a *directly*\n*21│//!   elevated `spt daemon` — Linux drops privileges in-process\n 22│//!   ([`SudoInvoker::drop_in_process`]) before touching any state; Windows\n*23│//!   respawns de-elevated and exits.\n 24│//!\n*25│//! When no unelevated identity exists to drop to (UAC disabled, genuine\n 26│//! root login, SYSTEM), callers run as-is with a loud warning — a\n*27│//! consistent universe, never a torn one. Elevated one-shot *clients*\n*28│//! talking downward to an unelevated daemon are fine; the daemon side is\n 29│//! the invariant.\n 30│//!\n*31│//! The OS probes mirror `spt`'s REQ-PAIR-6 `elevation.rs` (the CLI gate);\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 37│\n 38│#[cfg(unix)]\n 39│pub use unix::{daemon_target_user, sudo_invoker, SudoInvoker};\n   │...\n 41│#[cfg(windows)]\n*42│pub use windows::{has_deelevation_target, is_elevated, spawn_deelevated};\n 43│\n*44│/// What a spawner should do about elevation — the pure decision seam (the\n 45│/// REQ-PAIR-6 `gate_for` posture: the OS probe is environment-dependent,\n*46│/// the decision is unit-tested both ways).\n 47│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 48│pub enum SpawnPlan {\n### digest.rs\n  1│//! The session-digest **log projection** — the daemon-side, on-demand projection\n *2│//! of an endpoint's live activity buffer from its normalized session logs\n  3│//! (ADR-0008 amendment, 2026-06-12; REQ-TERM-4 log-source re-foundation).\n  4│//!\n *5│//! ## On-demand projection, not a byte feed\n  6│//!\n *7│//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n *8│//! on every output chunk. The digest is now a **projection of the same normalized\n *9│//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n 10│//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n 11│//! records via [`project_endpoint_digest`], and the hub publishes the result.\n*12│//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n 13│//! read the logs.\n 14│//!\n 15│//! ## Sources (one per endpoint)\n   │...\n 17│//! - An adapter that declares `[history]` (Path A locate+normalize / fetcher, or\n*18│//!   Path B native store) → the projection tails those normalized records. This\n*19│//!   is the **topology-independent** path: a Claude-Code-hosted endpoint's\n*20│//!   per-session JSONL is Path A's worked example.\n 21│//! - A **log-less** adapter pushes digest-records directly via `api digest-entry`\n 22│//!   into the perch's `digest.log` → the projection reads that.\n 23│//!\n*24│//! The records carry the published digest-record contract\n*25│//! ([`spt_term::DigestRecord`]); the projection folds them into the structured\n*26│//! [`Digest`] (N-turn window, collapsed tool sprints, ~25-char arg truncation).\n 27│//!\n*28│//! ## Snapshot vs delta\n 29│//!\n*30│//! A projection is diffed against the hub's prior snapshot\n 31│//! ([`crate::digesthub::DigestHub::project_and_publish`]) to produce a\n*32│//! [`DigestUpdate`]: `from` is where the changed turns begin, and the subscriber\n 33│//! applies it by **truncating its view to `from`, then appending `turns`**. The\n*34│//! delta-stream **contract** ships; its deltas are driven by pull / digest-entry\n*35│//! publishes (autonomous file-watch freshness is deferred to the consuming\n 36│//! frontend milestone — CONTEXT.md \"session digest\").\n 37│// [impl->REQ-TERM-4]\n 38│\n   │...\n 45│use spt_runtime::manifest::Digest as ManifestDigest;\n*46│use spt_store::perch::{self, ParentHint};\n*47│use spt_term::{Digest, DigestConfig, TimelineItem, Turn};\n 48│\n 49│/// Bound for the projection's extractor run (the `[digest]` extractor is a\n*50│/// subprocess — REQ-HAZARD-SUBPROCESS-TIMEOUT; a hang is killed, never blocks).\n 51│const DIGEST_FETCH_TIMEOUT: Duration = Duration::from_secs(30);\n 52│\n### digesthub.rs\n *1│//! The daemon-held digest hub + its control channel (ADR-0008 amendment /\n  2│//! REQ-TERM-4).\n  3│//!\n *4│//! The hub holds the latest projected snapshot per endpoint and fans every change\n *5│//! out to live subscribers. The digest is **projected on demand** from the\n *6│//! endpoint's normalized records ([`project_endpoint_digest`]) — there is no live\n *7│//! feed; a pull or a `digest-entry` push triggers a projection, and\n  8│//! [`DigestHub::project_and_publish`] diffs it against the prior snapshot to\n *9│//! produce the [`DigestUpdate`] delta. The control channel is the access surface\n 10│//! ADR-0008 describes — two modes over one transport:\n 11│//!\n 12│//! - **snapshot pull** — `spt endpoint digest <id>` ([`pull_snapshot`]) projects\n 13│//!   the endpoint's current buffer on demand and returns it;\n*14│//! - **delta-stream** — `spt endpoint digest <id> --follow` ([`follow`])\n 15│//!   subscribes, receives a base snapshot, then **only the changes**\n 16│//!   ([`DigestUpdate`]s) as pulls / `digest-entry` pushes re-project. The stream\n*17│//!   contract ships; autonomous file-watch freshness is deferred to the consuming\n 18│//!   frontend milestone (CONTEXT.md \"session digest\").\n 19│//!\n*20│//! The channel rides the same forward-compatible [`Envelope`] + codec + version\n*21│//! handshake as the broker / seed-control IPC (a transient `spt` CLI connects as a\n*22│//! [`Role::Brain`] logic client). **Local addressing only** — the qualified\n*23│//! `[subnet:]id@node` cross-node form is a later milestone (rejected by the CLI\n 24│//! before it ever reaches here).\n 25│// [impl->REQ-TERM-4]\n 26│\n 27│use std::collections::HashMap;\n*28│use std::io::{self, Read, Write};\n*29│use std::sync::mpsc::{channel, Receiver, Sender};\n 30│use std::sync::{Arc, Mutex};\n 31│use std::thread;\n 32│\n   │...\n 37│use crate::digest::{common_prefix_len, project_endpoint_digest, DigestOverride, DigestUpdate};\n*38│use crate::frame::{Envelope, Role};\n*39│use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};\n 40│\n*41│/// Client→daemon: pull the current structured digest for an endpoint (projects\n*42│/// it on demand from the endpoint's normalized records).\n 43│pub const KIND_DIGEST_SNAPSHOT: &str = \"digest_snapshot\";\n 44│/// Daemon→client: the snapshot reply (digest + version; absent ⇒ no digest).\n 45│pub const KIND_DIGEST_RESULT: &str = \"digest_result\";\n*46│/// Client→daemon: subscribe to an endpoint's delta-stream from a version floor.\n 47│pub const KIND_DIGEST_SUBSCRIBE: &str = \"digest_subscribe\";\n*48│/// Daemon→client: one structured digest change (the streamed delta / base).\n\n### bin/\n#### xlate_choreo_fixture.rs\n  1│//! W2 Layer G integration fixture: a REAL ADR-0022 translation binary —\n *2│//! a pure stdin→stdout JSON-lines filter spt-core lifecycle-manages. Used by\n *3│//! `tests/inject_control_wedge.rs` (referenced via\n  4│//! `env!(\"CARGO_BIN_EXE_xlate_choreo_fixture\")`). NOT a production binary.\n  5│//!\n  6│//! Protocol (spt-core → here, one JSON object per line on stdin):\n *7│//!   `{\"type\":\"init\",…}`  — identity; noted, never drives output.\n *8│//!   `{\"type\":\"input\"}`   — a CONTENT-FREE operator-activity ping; ignored.\n *9│//!   `{\"type\":\"event\",\"envelope\":\"…\"}` — an inbound `<EVENT>`; we respond with\n 10│//!                          the CC stash/restore choreography.\n 11│//!\n 12│//! On an `event` we emit (here → spt-core, one JSON object per line on stdout,\n 13│//! flushing each so the broker's reader sees them promptly):\n*14│//!   `{\"key\":\"ctrl+s\"}` `{\"delay_ms\":50}` `{\"text\":\"XLATE_OK\"}` `{\"key\":\"enter\"}`\n*15│//!   `{\"delay_ms\":50}` `{\"key\":\"ctrl+s\"}` then `{\"commit\":true}`.\n 16│//!\n 17│//! Modes (env `XLATE_FIXTURE_MODE`, read ONCE at start; default `choreo`):\n 18│//!   - `choreo`   — the full sequence ENDING in `{\"commit\":true}` (happy path).\n 19│//!   - `nocommit` — emit the payload text + a key but NEVER `{\"commit\":true}`, so\n*20│//!     the broker's commit-deadline FAULT path is exercised (every event misses).\n 21│//!   - `nocommit_once` — MISS the FIRST event (no `{\"commit\":true}`) then behave as\n 22│//!     `choreo` (commit) for every subsequent event. Exercises the F029 C-1\n*23│//!     miss≠fault tolerance: one missed commit must NOT terminate a healthy binary,\n*24│//!     and the NEXT inject must deliver through the SAME surviving binary.\n 25│//!   - `bigmultiline` — the choreo sequence but the `{\"text\":…}` payload is a LARGE\n*26│//!     MULTI-LINE block bracketed by `HEADSTART`…`TAILEND` markers (W5-A\n*27│//!     REQ-INJECT-MULTILINE-INTEGRITY): a single big typed payload the broker's\n*28│//!     settle-gate + chunked write must deliver byte-complete (head AND tail intact)\n 29│//!     across a LOOP of injects.\n 30│//!\n 31│//! Privacy assertion support (doyle gate #1): every stdin line we receive is\n*32│//! appended verbatim to the file named by env `XLATE_FIXTURE_STDIN_LOG` (when\n 33│//! set). The int test reads that file and asserts NO operator keystroke payload\n*34│//! (e.g. the controller's typed bytes) ever appears — only `init` / `event` /\n 35│//! content-free `input` lines do. The binary never sees PTY keystroke content.\n 36│\n 37│use std::fs::OpenOptions;\n*38│use std::io::{self, BufRead, Write};\n 39│\n*40│/// The fixed, recognizable payload token the choreography types into the PTY.\n 41│const PAYLOAD: &str = \"XLATE_OK\";\n 42│\n 43│fn main() {\n*44│    let mode = std::env::var(\"XLATE_FIXTURE_MODE\").unwrap_or_else(|_| \"choreo\".to_string());\n 45│    let stdin_log = std::env::var(\"XLATE_FIXTURE_STDIN_LOG\").ok();\n 46│\n 47│    let stdin = io::stdin();\n   │...\n 53│    for line in stdin.lock().lines() {\n*54│        let Ok(line) = line else { break }; // stdin closed (EOF) → exit (no-zombie).\n*55│        let trimmed = line.trim();","truncation":{"content":"# crates/spt-daemon/\n## Cargo.toml\n 3|version.workspace = true\n*4|edition = \"2021\"\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|# Test-only REDISPATCH-TRUTH dispatcher-generation fixture for\n*9|# `tests/redispatch.rs` (REQ-HAZARD-REDISPATCH-CONTROL-STEAL /\n 10|# REQ-STREAM-OPENER-DURABLE / REQ-DISPATCH-CLAIM-RETRY): the REAL\n*11|# `run_dispatch_loop` as a KILLABLE child process — a killed generation drops\n 12|# its workers' broker conns exactly like a production brain death. Same\n*13|# tests/-homed `[[bin]]` pattern as spt's fixtures (excluded from the release\n 14|# build, which is `--bin spt` only). Name avoids the substring \"update\"\n 15|# (the Windows installer-detection UAC-740 class).\n 16|[[bin]]\n...\n 20|[dependencies]\n*21|# Layer-below set (R-ARCH-1 acyclic): …→spt-live→spt-daemon→spt. The broker\n*22|# re-hosts spt-term PTYs + the spt-live lifecycle seams; these edges fix the\n 23|# crate's position in the workspace layering. Tasks B1–B8 consume them.\n 24|spt-proto = { path = \"../spt-proto\" }\n 25|spt-store = { path = \"../spt-store\" }\n...\n 34|spt-net = { path = \"../spt-net\" }\n*35|# The broker is otherwise sync/thread-based; iroh requires tokio, so the broker\n*36|# hosts a dedicated runtime on named worker threads (QUIC objects never leave it).\n 37|tokio = { version = \"1\", features = [\"rt-multi-thread\", \"net\"] }\n 38|# The node-local docs server (ADR-0036 §4, REQ-DOCS-LOCAL-SERVER): hyper +\n*39|# its util crates are ALREADY in the lock via iroh/reqwest — battle-tested\n*40|# crate over hand-rolled HTTP (operator-ruled), zero new supply chain.\n 41|hyper = { version = \"1\", features = [\"server\", \"http1\"] }\n 42|hyper-util = { version = \"0.1\", features = [\"tokio\"] }\n 43|http-body-util = \"0.1\"\n...\n 45|serde_json = { version = \"1\", features = [\"preserve_order\"] }\n*46|# PTY output is arbitrary bytes; the forward-compat JSON envelope carries it as\n 47|# a base64 string in the payload rather than forking the codec for binary.\n 48|base64 = \"0.22\"\n*49|# Cross-platform local IPC: named pipe (Windows) / Unix-domain socket (unix)\n 50|# behind one API — the OS-split substrate the DaemonTransport trait wraps,\n 51|# mirroring how spt-term abstracts ConPTY vs forkpty behind SessionSurface.\n 52|interprocess = \"2\"\n*53|# Self-update release signing (C1, REQ-UPD-2): pure-Rust Ed25519 — no C/asm\n 54|# build deps, clean cross-compile on win+linux (clean-room posture, ADR-0004 §D).\n*55|# `rand_core` lets a release-tooling caller mint a keypair; tests use the\n 56|# deterministic `from_bytes` ctor (no RNG).\n*57|ed25519-dalek = \"2\"\n*58|# SHA-256 binds the signed release metadata to the actual binary bytes\n 59|# (artifact digest) so the signature covers the binary, not just the manifest.\n 60|sha2 = \"0.10\"\n*61|# Per-frame link auth on the same-node shell channels (CONTEXT local-link\n*62|# authentication): HMAC-SHA256 over the token-derived link key (M5-D3b).\n 63|hmac = \"0.12\"\n 64|\n\n## src/\n### access.rs\n 2|//!\n*3|//! ADR-0009 models access as **three orthogonal, nested gates**, evaluated in\n 4|//! order: subnet *visibility* (is the endpoint routable here at all? —\n*5|//! `spt_store::visibility`, applied inside registry resolution) → the\n*6|//! **endpoint access whitelist** (may *your node* remotely reach it? — THIS\n 7|//! module) → *capability grants* (may this agent do this high-risk thing? —\n*8|//! [`crate::consent`], default-deny). The whitelist is the outer gate before\n*9|//! grants: an origin the whitelist refuses never reaches a grant check.\n 10|//!\n*11|//! ## Whitelist semantics (stateful firewall, not a blanket block)\n 12|//!\n 13|//! - **Origin-node gate:** the subject is the node the inbound interaction\n 14|//!   *originates from* (where the operator is acting) — never the sender\n...\n 16|//!   handshake-proven remote node id from the broker's conn/stream table,\n*17|//!   never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).\n*18|//! - **Outbound** → any visible node: the whitelist never restricts who an\n 19|//!   endpoint talks *to*.\n*20|//! - **Inbound reply** correlated to the endpoint's own prior outbound\n*21|//!   (keyed on the inbound `from`) → allowed from any visible node\n*22|//!   (\"established/related\").\n 23|//!   Classification reads the durable [`RecentOutbound`] log\n*24|//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].\n*25|//! - **Unsolicited/direct inbound** → only from a whitelisted node.\n*26|//! - **Same-node operation is always allowed** — the gate sits on the\n*27|//!   net-inbound path only, so the local delivery path never sees it; an\n*28|//!   origin that *is* this node (a loopback self-dial) short-circuits Allow.\n*29|//! - **Default absent = open**; setting a whitelist restricts (an empty entry\n*30|//!   = locked down). Node-tier ships M4 (Ed25519 node pubkeys); the user tier\n*31|//!   is an inert schema reservation until the cross-user model (ADR-0006).\n 32|//!\n 33|//! The store is [`spt_store::access::AccessStore`] (`trust/access.json` —\n 34|//! security material next to the trust store). The gate re-reads it per check:\n*35|//! an `spt access` edit takes effect on the next inbound without a daemon\n 36|//! restart, and the checks run at message/stream-establishment rate where a\n 37|//! small-file read is noise.\n 38|//!\n### adapter_update.rs\n*1|//! Adapter ripple-update conduct (C3) — spt-core updates the whole stack: self\n*2|//! first, then each registered adapter via its manifest `[update]` avenue, with\n 3|//! **adapter content signing** (REQ-UPD-5, REQ-SEAM-UPDATE, ADR-0004 §D).\n 4|//!\n 5|//! After spt-core's own update completes (the C0–C2 engine), it conducts a\n*6|//! ripple over its registered adapters. Each adapter declares how it updates in\n 7|//! its manifest `[update]` block ([`Update`]); this module turns that directive\n*8|//! into a per-adapter [`AdapterUpdateOutcome`], dispatched by avenue:\n 9|//!\n 10|//! - **`file_pull`** — spt-core controls the bytes, so it *verifies* them: the\n*11|//!   pulled payload is checked against the adapter's **own** Ed25519 key (declared\n*12|//!   in the manifest — the adapter author signs their releases; spt-core's release\n*13|//!   key stays scoped to spt-core) plus the SHA-256 artifact binding. Authentic +\n*14|//!   intact ⇒ [`AdapterUpdateOutcome::Verified`]; otherwise\n*15|//!   [`AdapterUpdateOutcome::Rejected`].\n*16|//! - **`delegated`** — the adapter's own updater installs the content (e.g.\n 17|//!   `claude.exe plugin update`); spt-core never sees those bytes, so there is\n*18|//!   nothing for it to hash. It delegates the trust **only** when the adapter\n*19|//!   attests `self_verifies` (its updater checks its own signatures) ⇒\n*20|//!   [`AdapterUpdateOutcome::Delegate`]; an unattested delegated update is\n*21|//!   [`AdapterUpdateOutcome::Skipped`] as unverifiable.\n 22|//!\n 23|//! The actual byte transport for `file_pull` (the network pull) is M4\n*24|//! (`REQ-UPD-1`); v1 receives the payload via self-fetch / out-of-band and feeds\n 25|//! it here. This module is the **decision + verification** layer — it does not\n*26|//! itself fetch or exec.\n 27|// [impl->REQ-UPD-5]\n 28|// [impl->REQ-SEAM-UPDATE]\n 29|\n...\n 31|\n*32|use crate::release::{\n*33|    parse_verifying_key, verify_artifact, verify_signature, RejectReason, ReleaseMetadata,\n*34|    SignedRelease,\n 35|};\n 36|\n*37|/// A pulled `file_pull` payload awaiting verification: the adapter's signed\n### applyhost.rs\n*1|//! Staged-update apply orchestration (M5-D7b) — the **production caller** for\n 2|//! [`crate::update::apply_brain_only`], closing the M4-D9 honest residual\n 3|//! (\"nothing in the daemon invokes this apply after the user acks the consent\n 4|//! notif\").\n...\n 9|//! notif and is a *prompt* answer, not a lock — an apply after a dismiss\n*10|//! still works, the user changed their mind).\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*16|//!    `release-keys.json` policy): apply never trusts the cache — a tampered\n*17|//!    or expired artifact refuses here. The monotonic-version floor was\n*18|//!    enforced at *stage* time (REQ-HAZARD-UPDATE-ROLLBACK); the apply-time\n 19|//!    re-check guards authenticity/integrity/expiry of the bytes on disk.\n 20|//! 2. **Classify** against the running broker's ABI. Only\n 21|//!    [`UpdateClass::BrainOnly`] applies in M5 — the broker-touching classes\n*22|//!    refuse typed, the artifact stays staged (their consent-and-scheduling\n 23|//!    orchestration is post-M5; ADR-0004 §A).\n 24|//! 3. **Swap the binary**: the current executable steps aside\n 25|//!    (`<exe>.old-<ver>` — a running image survives its own rename on both\n*26|//!    OSes) and the verified artifact lands at the executable path\n*27|//!    atomically. A failed landing rolls the original back — never a\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 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 41|// [impl->REQ-UPD-4]\n...\n 47|use crate::brain::{now_ms, Brain};\n*48|use crate::relcache::{AppliedRecord, ReleaseCache, StagedUpdate};\n*49|use crate::release::{current_platform, VerifyPolicy};\n*50|use crate::update::{plan_verified, plan_verified_update_set, BrokerAbi, UpdateClass};\n 51|\n*52|/// What [`apply_staged`] did — loud + typed, never silent.\n 53|#[derive(Debug, Clone, PartialEq, Eq)]\n### attach.rs\n 3|//!\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*9|//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n 10|//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n*11|//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n*12|//!   journal (a replayed input record cannot double-type);\n 13|//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.\n 14|//!\n*15|//! ## Exactly-once lives at the EDGES, not the wire sends\n 16|//!\n*17|//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a\n*18|//! journaled [`Brain::net_stream_send`] blocks reading events until its ack\n*19|//! and *discards* everything interleaved — inside a full-duplex pump loop that\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*25|//! re-transmit output records on the wire — duplicates are dropped at the\n*26|//! render cursor, the same discipline the local `OutputLog` replay uses\n 27|//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).\n 28|//!\n 29|//! **Compute + files stay here**; the operator is a viewport (CONTEXT\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*35|//! the D5d whitelist drops in at this same call.\n 36|//!\n 37|//! Restart survival composes from D4/B5 with **no new machinery**: the broker\n 38|//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.\n*39|//! A restarted target brain re-attaches the session from its output cursor,\n*40|//! resubscribes the stream from its net cursor, and re-drives journaled sends;\n*41|//! a restarted operator brain resubscribes and dedups rendered chunks by\n 42|//! `Output.seq`.\n 43|\n 44|use std::io;\n### autostart.rs\n 4|//! always-there, but a box reboot / daemon cold start left it down until\n*5|//! hands-on (deployah field-confirm 2026-07-10, post-v0.30.6 restart). The\n 6|//! operator marks an endpoint a **startup default** with `spt endpoint run\n*7|//! --save` (the endpoint analog of the shipped `subnet attach --save`\n*8|//! precedent — doyle-ruled shape (a)); the daemon replays the saved runs here\n 9|//! at start.\n 10|//!\n 11|//! Contract: a replay is best-effort and LOUD — an unresolvable adapter or a\n*12|//! failed launch logs its skip and moves on; nothing here can block daemon\n 13|//! start or the other replays. Each replay is a FRESH session (shape (c)\n*14|//! restore-what-was-up was rejected): the adapter option re-resolves against\n*15|//! the live registry, the session id is freshly minted, and home rides the\n 16|//! endpoint's existing perch (durable — REQ-INST-15), so no `--subnet` is\n*17|//! recorded or needed.\n 18|\n 19|use crate::config::DaemonConfig;\n 20|\n*21|/// How long to keep retrying a launch whose broker round-trip failed — the\n 22|/// replay thread starts alongside the broker's serve thread, so the very first\n*23|/// connect can race the accept loop up. Bounded and short: a genuine launch\n 24|/// failure (dead adapter binary, bad template) fails again instantly and we\n 25|/// move on after the last attempt.\n 26|const LAUNCH_ATTEMPTS: u32 = 3;\n*27|const LAUNCH_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(750);\n 28|\n*29|/// Replay every saved startup default against the (just-bound) broker. Runs on\n 30|/// a daemon-boot background thread — never returns an error, never panics the\n 31|/// daemon; every outcome is a loud stderr token:\n*32|/// - `ENDPOINT_AUTOSTART:<id>` — the saved run came up;\n*33|/// - `ENDPOINT_AUTOSTART_SKIP:<id>` — the saved adapter no longer resolves to a\n*34|///   registered harness (the save is stale; re-run `endpoint run --save`);\n*35|/// - `ENDPOINT_AUTOSTART_FAIL:<id>` — the launch itself failed after retries.\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...\n 43|    // `{node}` fill: the same OS-hostname fallback the CLI spawn path uses —\n*44|    // the in-mem label handle lives daemon-side but this thread predates it.\n*45|    let node_label = spt_store::hostlabel::node_fill_label(None);\n 46|    for entry in entries {\n*47|        // Re-resolve the adapter option fresh (an adapter updated between\n*48|        // restarts is honored; an uninstalled one is a loud skip, never a wedge).\n 49|        let (record, manifest) =\n 50|            match spt_runtime::registry::resolve_option(&adapters_dir, &entry.adapter) {\n 51|                Ok((r, m)) if m.adapter.kind == spt_runtime::manifest::AdapterKind::Harness => {\n...\n 55|                    eprintln!(\n*56|                        \"ENDPOINT_AUTOSTART_SKIP:{}: saved adapter '{}' is not a harness \\\n 57|                         adapter — re-save with `spt endpoint run --save`\",\n 58|                        entry.id, entry.adapter\n 59|                    );\n### brain.rs\n 2|//!\n*3|//! The brain holds all the volatile logic; it may be killed and restarted at\n*4|//! any moment (a routine self-update swaps it; a crash kills it). It connects to\n*5|//! the stable [`crate::broker::Broker`] over the versioned IPC, drives sessions\n 6|//! (spawn / input / resize / kill), and consumes their output. Two invariants\n 7|//! this type encodes:\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 21|//!   error — so a lost chunk can never pass silently.\n 22|// [impl->REQ-HAZARD-GEN-START-NOW]\n...\n 25|use std::io;\n*26|use std::sync::mpsc::{self, Receiver, RecvTimeoutError};\n*27|use std::thread::{self, JoinHandle};\n 28|use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n 29|\n*30|use interprocess::local_socket::prelude::*;\n 31|\n 32|use interprocess::local_socket::{SendHalf, Stream};\n 33|\n 34|use crate::codec::{read_frame, write_frame};\n*35|use crate::frame::{Envelope, Role};\n*36|use crate::effect::MintedOp;\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*42|    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n*43|    NetStreamSubscribeReq, NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, NetStreamUnsubscribeReq, NetStreamUnsubscribed, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n*44|    SessionsReply, SizeEvent, SpawnConflict, SpawnReq, Spawned, StreamLifetime, SubscribeOutcome,\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*5|//! drifted to an in-process thread (`daemon.rs`), so `spt update apply` swaps\n 6|//! the binary on disk but never restarts the running code (the regression\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 12|//! consumers** (inbound dispatcher + outbound peer pump — pure IPC clients, no\n*13|//! shared `Arc<Broker>`) into the brain, gated on the existing `net-status`\n*14|//! verb. The digest hub serve + seed-control stay broker-side continuity\n 15|//! resources (ADR-0018 Q2/Q5); later D2 commits move the digest parse + psyche\n 16|//! loops.\n 17|//!\n...\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 25|use std::io;\n...\n 28|use std::sync::atomic::{AtomicBool, Ordering};\n*29|use std::sync::{Arc, OnceLock};\n 30|use std::thread;\n 31|use std::time::{Duration, Instant};\n 32|\n...\n 34|use crate::endpoint::{brain_ready_path, broker_socket_name};\n*35|use crate::relcache::{AppliedRecord, ReleaseCache};\n 36|\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 42|/// never a hot respawn loop.\n 43|const SUPERVISE_BACKOFF_CAP: Duration = Duration::from_secs(60);\n 44|/// A run at least this long counts as healthy and resets the backoff floor.\n*45|/// Reused as the per-attempt **trial window** (ADR-0018 Q7, D6-2): a candidate\n 46|/// brain has this long to signal `ready` for its generation before the window\n*47|/// elapses (the \"up but useless\" alive-never-ready rollback path).\n 48|const SUPERVISE_HEALTHY_RUN: Duration = Duration::from_secs(30);\n 49|/// The readiness-trial retry budget (ADR-0018 Q7, D6-2): a candidate that\n*50|/// **exits before** signaling ready is respawned this many consecutive times\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 56|/// Poll/sleep slice so a `stop` request (and a child exit) land promptly.\n...\n 59|/// How long the brain child retries connecting to the broker socket before\n*60|/// giving up (the broker may still be binding — the child is freshly spawned).\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### broker.rs\n*1|//! The broker kernel (B1 + B2) — the stable half of the broker/brain split.\n 2|//!\n 3|//! Per ADR-0004 §B the broker holds **only** the resources a live consumer\n*4|//! would lose continuity on if the logic restarted: the PTY master+writer and\n*5|//! the spawned child (both via [`spt_term::PtySession`]), and the accepted\n 6|//! client / listening sockets. It runs **no logic** — no routing, registry,\n*7|//! manifest parse, or lifecycle. It is a relay: spawn a child under a PTY,\n 8|//! forward its output to the brain, inject the brain's input, report exit.\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 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*18|//!   [`OutputLog`]: every chunk gets a monotonic per-session `seq` and is held\n*19|//!   in a bounded ring. The current subscriber (a brain connection) receives\n 20|//!   chunks live; all subscriber writes happen *under the log lock*, so frames\n 21|//!   reach the brain in strict `seq` order with no interleave.\n*22|//! - **Resume from a cursor.** A reconnecting brain sends `subscribe` with its\n 23|//!   last-seen `seq + 1`; the broker replays the ring from there, then streams\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 29|//! brain disconnect only detaches the subscriber; the child, its PTY, and the\n 30|//! output log persist. A session leaves the table only when its child exits\n*31|//! (reaped by the exit waiter) or is explicitly killed.\n 32|// [impl->REQ-DAEMON-2]\n 33|\n 34|use std::collections::{HashMap, HashSet, VecDeque};\n...\n 36|use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};\n*37|use std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};\n*38|use std::sync::{Arc, Mutex, OnceLock};\n*39|use std::thread::{self, JoinHandle};\n 40|use std::time::{Duration, Instant};\n 41|\n*42|use interprocess::local_socket::{prelude::*, Stream};\n 43|use spt_term::{Drain, PtySession, ScreenGrid, SessionSurface, SurfaceSize};\n 44|\n 45|use spt_net::net::attach::AttachIntent;\n...\n 51|use crate::effect::{EffectJournal, EffectKey, EffectKind, Minter, Outcome};\n*52|use crate::frame::{Envelope, Role};\n 53|use crate::msg::{\n*54|    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,\n### codec.rs\n*1|//! Length-prefixed framing for the broker↔brain IPC.\n 2|//!\n 3|//! Frame: a 4-byte big-endian length prefix, then that many bytes of UTF-8\n*4|//! JSON encoding one [`Envelope`]. Same shape as `spt-msg::wire` (ADR-0001\n 5|//! framing) but generic over any [`Read`]/[`Write`] — the IPC carrier is a\n*6|//! local socket (named pipe / UDS), not a `TcpStream`, and tests exercise it\n 7|//! over in-memory buffers and socket pairs.\n 8|//!\n*9|//! The frame *codec* is deliberately transport-agnostic: it is the byte-level\n 10|//! contract, while [`crate::transport`] owns the OS-specific connection.\n 11|// [impl->REQ-DAEMON-2]\n 12|\n*13|use std::io::{self, Read, Write};\n 14|\n*15|use crate::frame::Envelope;\n 16|\n*17|/// Maximum framed message size accepted (16 MiB) — guards a garbage/hostile\n 18|/// length prefix from forcing a huge allocation. Matches `spt-msg::wire`.\n 19|const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;\n 20|\n*21|/// Serialize and write one [`Envelope`] as a length-prefixed JSON frame.\n*22|pub fn write_frame<W: Write>(w: &mut W, env: &Envelope) -> io::Result<()> {\n 23|    let body =\n 24|        serde_json::to_vec(env).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 25|    if body.len() > MAX_FRAME_BYTES {\n...\n 37|\n*38|/// Read one length-prefixed JSON frame and decode it into an [`Envelope`].\n*39|/// Rejects an oversized length prefix before allocating. A clean EOF on the\n*40|/// length prefix surfaces as [`io::ErrorKind::UnexpectedEof`] (peer hung up).\n*41|pub fn read_frame<R: Read>(r: &mut R) -> io::Result<Envelope> {\n 42|    let mut len_buf = [0u8; 4];\n 43|    r.read_exact(&mut len_buf)?;\n 44|    let len = u32::from_be_bytes(len_buf) as usize;\n...\n 64|    fn frame_round_trips_through_buffer() {\n*65|        let env = Envelope::new(\"attach\", json!({ \"session\": \"s1\", \"n\": 7 }));\n 66|        let mut buf: Vec<u8> = Vec::new();\n 67|        write_frame(&mut buf, &env).unwrap();\n 68|\n*69|        let mut cursor = io::Cursor::new(buf);\n*70|        let got = read_frame(&mut cursor).unwrap();\n 71|        assert_eq!(got.protocol_version, IPC_PROTOCOL_VERSION);\n 72|        assert_eq!(got.kind, \"attach\");\n 73|        assert_eq!(got.payload, json!({ \"session\": \"s1\", \"n\": 7 }));\n...\n 75|\n*76|    // [unit->REQ-DAEMON-2] multiple frames stream back-to-back without bleed.\n 77|    #[test]\n 78|    fn back_to_back_frames_decode_independently() {\n 79|        let mut buf: Vec<u8> = Vec::new();\n*80|        write_frame(&mut buf, &Envelope::new(\"a\", json!(1))).unwrap();\n*81|        write_frame(&mut buf, &Envelope::new(\"b\", json!(2))).unwrap();\n 82|\n*83|        let mut cursor = io::Cursor::new(buf);\n### config.rs\n 3|//!\n*4|//! In the no-daemon interim model the pulse cadence was a hard-coded\n 5|//! `spt_live::pulse::DEFAULT_PULSE_PERIOD` (5s). Consolidation (REQ-DAEMON-1)\n*6|//! folds the pulse loop into the brain as a *scheduled* in-process loop whose\n*7|//! period is **durable and configurable**, sourced from a small JSON file under\n 8|//! the spt home (`<spt_home>/daemon.json`) and falling back to the default when\n*9|//! absent. Kept deliberately tiny — one struct, JSON via the crate's existing\n*10|//! `serde_json` dep (no new TOML edge) — so the brain's loop reads its cadence\n 11|//! from config, never a constant.\n 12|// [impl->REQ-DAEMON-1]\n 13|\n...\n 18|\n*19|/// The interim default pulse period carried forward as the config default. The\n 20|/// daemon now treats this as a *fallback*, not a constant: `daemon.json` may\n 21|/// override it per machine.\n 22|pub const DEFAULT_PULSE_PERIOD: Duration = Duration::from_secs(5);\n 23|\n*24|/// Default drop-file precedence protection window (epoch-ms), mirroring the\n*25|/// interim `spt_live` guard. Surfaced here so the one config source owns it.\n 26|pub const DEFAULT_PROTECTION_WINDOW_MS: u64 = 2_000;\n 27|\n*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 35|/// Default peer-pump cadences (D9-1): how often the daemon pushes its\n*36|/// registry/notif feeds to trusted peers and pulls context syncs from them.\n 37|/// Conservative defaults — every loop is a thin call over an idempotent\n*38|/// driver, so a missed tick costs only latency, never correctness.\n 39|pub const DEFAULT_REGISTRY_PUMP_PERIOD: Duration = Duration::from_secs(30);\n 40|pub const DEFAULT_NOTIF_PUMP_PERIOD: Duration = Duration::from_secs(15);\n 41|pub const DEFAULT_SYNC_PULL_PERIOD: Duration = Duration::from_secs(60);\n...\n 44|/// Default silent-peer registry eviction window (KNOWN-HAZARDS 4.10): rows\n*45|/// authored by a node not heard within this window are evicted on the next\n*46|/// registry pump tick. Sized to ~10 default pump cadences — generous against\n*47|/// transient drops (a peer re-advertises every cadence, so one admitted feed\n 48|/// inside the window keeps its rows), short enough that a dead node identity\n 49|/// stops poisoning bare-id resolution within minutes.\n 50|pub const DEFAULT_REGISTRY_EVICT_AFTER: Duration = Duration::from_secs(300);\n 51|\n*52|/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —\n*53|/// the `\"relay\"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in\n 54|/// `Daemon::run`; kept as a local enum so the config layer stays transport-free.\n*55|/// The self-hosted variant (`RelayPolicy::SelfHosted`) needs a relay-map URL\n### conn.rs\n*1|//! Bounded, cancelable, poison-on-failure framed writes for one physical\n*2|//! broker connection (UPDATE-WEDGE round-4).\n 3|//!\n 4|//! ## The failure class this kills\n 5|//!\n*6|//! A broker writer used to hold the connection's `Mutex<SendHalf>` guard\n 7|//! ACROSS a blocking [`write_frame`]. On Windows, `interprocess 2.4.2` routes\n 8|//! that send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — a local-socket\n*9|//! path with **no supported write timeout** (`set_timeout` → `Unsupported`).\n*10|//! A connected consumer that stops reading (an `rc --take` whose process\n*11|//! wedged; a suspended brain) therefore blocks the write INDEFINITELY, and the\n*12|//! logical stall-evict (`stall_evict_controller`) releases only the controller\n*13|//! ROLE — it never cancels the in-flight pipe write or closes the physical\n*14|//! connection, so the writer (and every write queued behind the same gate)\n*15|//! stays wedged until the peer process dies (~128 s in the field capture, on a\n 16|//! brain restart).\n 17|//!\n 18|//! ## The primitive\n...\n 20|//! [`BrokerConn`] owns the send half outright — it never leaves this module,\n*21|//! so an unbounded bypass write cannot compile. Every write:\n 22|//!\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...\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### consent.rs\n*1|//! Self-update consent gating (C2) — the REQ-UPD-4 decision + the\n 2|//! most-recently-active-session resolver.\n 3|//!\n*4|//! ADR-0004 makes self-update **not fully automatic by default**: a verified,\n*5|//! eligible update is gated on a user confirmation delivered to the user's\n 6|//! **most-recently-active live session**, with an explicit opt-in to full-auto\n 7|//! (the seamless, prompt-free path). C2 ships the load-bearing, fully-testable\n 8|//! core of that:\n 9|//!\n*10|//! - [`decide`] — the **pure** gate: full-auto ⇒ [`ConsentDecision::AutoApproved`];\n*11|//!   otherwise [`ConsentDecision::NeedsConsent`] carrying the resolved target the\n 12|//!   orchestrator should prompt.\n 13|//! - [`most_recently_active`] — resolve that target over the **v1 registry**\n 14|//!   (live perches + their `last_active_ms` recency stamp). This is the\n*15|//!   registry-resolution precursor CONTEXT names; the deferred PresenceChannel\n 16|//!   later generalizes it.\n 17|//!\n*18|//! What C2 deliberately does **not** build: the interactive prompt UX itself.\n*19|//! That rides the deferred notification primitive (ADR-0007 — `update-available`\n*20|//! / `consent-needed` producers) and the harness's native confirm affordance\n*21|//! (e.g. AskUserQuestion). [`ConsentDecision::NeedsConsent`] is the typed seam\n 22|//! those plug into; nothing here reaches into a session.\n 23|// [impl->REQ-UPD-4]\n 24|\n...\n 26|\n*27|/// The session a consent prompt should be delivered to: the user's\n 28|/// most-recently-active live perch. Carries enough to address it (id + path) and\n*29|/// the recency that won it the selection (for logging / tie-break audit).\n 30|#[derive(Debug, Clone, PartialEq, Eq)]\n 31|pub struct ConsentTarget {\n 32|    /// The agent/perch id of the most-recently-active live session.\n 33|    pub id: String,\n*34|    /// Its resolved perch directory.\n 35|    pub perch_path: PathBuf,\n*36|    /// Its `last_active_ms` recency stamp (0 if never stamped — least-recent).\n 37|    pub last_active_ms: u64,\n 38|}\n 39|\n*40|/// The gate's verdict for a verified, eligible update (REQ-UPD-4).\n 41|#[derive(Debug, Clone, PartialEq, Eq)]\n 42|pub enum ConsentDecision {\n*43|    /// Full-auto is opted in — apply without a prompt (the seamless path).\n*44|    AutoApproved,\n*45|    /// Default-gated — hold the update until the user confirms. Carries the\n*46|    /// resolved session to prompt, or `None` when no live session could be\n*47|    /// resolved (the orchestrator then defers / falls back to a node-level surface\n### crc_swap.rs\n*1|//! W3b — content-hash-gated atomic per-file swap for a live adapter update\n 2|//! (ADR-0025, REQ-ADAPTER-LIVE-UPDATE).\n 3|//!\n 4|//! The legacy `spt adapter update` path does `remove_dir_all(install_dir)` then a\n*5|//! blanket extract — which fails on Windows with *\"Access is denied (os error 5)\"*\n*6|//! the moment a single install-dir binary is still running (the field bug). This\n*7|//! module replaces that with a **content-diff swap**: extract the staged archive\n 8|//! to a temp tree, replace in the install dir **only** the files whose content\n 9|//! hash differs (so identical files — and their still-running binaries — are\n*10|//! never touched), and commit via **stage-all-`.new`-then-one-tight-rename-loop**\n 11|//! so a mid-swap failure never strands the install half-new (doyle W3 gate,\n 12|//! binding partial-failure refinement).\n 13|//!\n*14|//! CRC here is plain content hashing (reuse [`crate::release::sha256_hex`], no new\n*15|//! dep) — \"replace only what changed\", not cryptographic integrity (that is the\n 16|//! archive signature, ADR-0024 / REQ-UPD-9).\n 17|\n*18|use crate::release::sha256_hex;\n 19|use std::path::{Path, PathBuf};\n 20|\n*21|/// One planned file replacement discovered by [`plan_crc_swap`].\n 22|#[derive(Debug, Clone, PartialEq, Eq)]\n*23|pub struct PlannedSwap {\n*24|    /// Path relative to the tree root (for diagnostics / determinism).\n*25|    pub rel: PathBuf,\n*26|    /// The staged source file (`<staging>/<rel>`).\n*27|    pub staged: PathBuf,\n*28|    /// The install-dir target (`<install_dir>/<rel>`).\n 29|    pub target: PathBuf,\n 30|}\n 31|\n...\n 33|/// `install_dir` counterpart — or that are absent in `install_dir`. Files whose\n*34|/// content is byte-identical are skipped, so a still-running unchanged binary is\n*35|/// never disturbed. Read-only: does not mutate either tree. The returned plan is\n*36|/// sorted by `rel` for deterministic apply + testing.\n 37|// [impl->REQ-ADAPTER-LIVE-UPDATE]\n*38|pub fn plan_crc_swap(staging: &Path, install_dir: &Path) -> std::io::Result<Vec<PlannedSwap>> {\n 39|    let mut plan = Vec::new();\n 40|    collect_diffs(staging, staging, install_dir, &mut plan)?;\n*41|    plan.sort_by(|a, b| a.rel.cmp(&b.rel));\n 42|    Ok(plan)\n 43|}\n 44|\n### daemon.rs\n 3|//! [`Daemon::run`] is the body of the hidden `spt daemon` subcommand: it binds\n*4|//! the broker (B1/B2 PTY hosting) and the seed-control channel (B6) under the\n 5|//! canonical per-home names ([`crate::endpoint`]) and serves both for the life\n 6|//! of the process.\n 7|//!\n 8|//! [`ensure_running`] is the auto-start seam (REQ-DAEMON-3): any `api`\n*9|//! invocation calls it; if no daemon answers the seed-control ping it spawns\n*10|//! `spt daemon` detached and waits (bounded) for it to come up. The reliable\n*11|//! anchor is the seed channel — bound for the whole daemon lifetime, so a ping\n*12|//! against it is a true liveness test (`listen` is the interim model's anchor;\n*13|//! here the seed channel plays that role for cold-start detection).\n 14|// [impl->REQ-DAEMON-3]\n 15|\n 16|use std::io;\n...\n 22|\n*23|use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};\n 24|\n 25|use crate::broker::{default_journal_path, Broker};\n*26|use crate::config::{DaemonConfig, RelayChoice};\n*27|use crate::digesthub::{self, DigestHub};\n 28|use crate::endpoint::{\n*29|    broker_socket_name, daemon_pid_path, digest_socket_name, drive_socket_name, seed_socket_name,\n 30|};\n 31|use crate::nethost::{NetConfig, NetHost};\n*32|use crate::seedmap::{self, SeedRegistry};\n 33|\n*34|/// How long auto-start waits for a freshly-spawned daemon to bind its channel.\n 35|const START_TIMEOUT: Duration = Duration::from_secs(5);\n*36|/// Poll cadence while waiting for the spawned daemon to come up.\n 37|const START_POLL: Duration = Duration::from_millis(25);\n 38|\n 39|/// The per-machine broker/brain daemon process.\n...\n 42|impl Daemon {\n*43|    /// Run the daemon: bind the broker (served on a background thread) + the\n*44|    /// seed-control channel (the foreground loop + liveness anchor) and serve\n*45|    /// until the process is killed. The body of `spt daemon`.\n 46|    pub fn run() -> io::Result<()> {\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*52|        // is touched or any child can be spawned.\n 53|        // [impl->REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE]\n 54|        spt_runtime::scrub_identity_env();\n*55|        // Anchor the monotonic half of the stderr correlation stamps at\n 56|        // process start (F-039 leg c). [impl->REQ-CONN-POISON-ATTRIBUTION]\n 57|        crate::conn::init_log_anchor();\n*58|        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n### deadline.rs\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 8|//!\n 9|//! Two shapes, two rules:\n 10|//!\n*11|//! - [`DeadlineAnchor`] — a **periodic** `(anchor, interval)` grid. Persisted\n*12|//!   ONCE per fresh/crash start; every fire is derived functionally\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*26|//!   BROKER-BRAIN-SPLIT-RESTORATION §7); the durable in-daemon scheduler that will\n*27|//!   drive it is the alarm-port deferral tracked in `docs/DEFERRED.md`. This\n*28|//!   helper is the *rule* the port reuses, not the *machinery*.\n 29|//!\n 30|//! **[V4] Only phase-significant loops convert.** The idempotent pump cadences\n*31|//! (`pump`) need none — they already stagger from everything-due-now,\n 32|//! catch-up/idempotent/restart-safe; converting them would re-add the per-loop\n 33|//! writes Q4 minimizes.\n 34|\n...\n 43|///\n*44|/// Keyed, not a singleton (ADR-0018 D5 amendment 6): a singleton would\n 45|/// cross-clobber the moment the live-agent adapter's **per-agent** pulse drivers\n*46|/// arrive (REQ-HAZARD-PER-AGENT-SCHEDULING / KH 7.4 — \"each agent drives its own\n 47|/// pulse\"). The daemon pulse uses key `\"pulse\"`; the adapter era passes the agent\n*48|/// id. Same code, parameterized path.\n 49|pub fn anchor_path(key: &str) -> PathBuf {\n 50|    spt_store::perch::spt_home().join(format!(\"deadline-{key}.json\"))\n 51|}\n...\n 65|impl DeadlineAnchor {\n*66|    /// Resolve a non-zero interval (doyle minor — ONE degrade rule, unit-tested):\n 67|    /// a zero `interval_ms` degrades to the config default pulse period (a `const`\n*68|    /// that is always `> 0`), so the derive can never divide by zero. The `else\n*69|    /// 1ms` floor is belt-and-braces should that default ever be made zero.\n 70|    fn effective_interval(interval_ms: u64) -> u64 {\n 71|        if interval_ms > 0 {\n 72|            return interval_ms;\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 87|    ///\n*88|    /// A corrupt file is treated as absent (degrade-to-fresh), matching\n 89|    /// `DaemonConfig::load_from` / `EffectJournal::open`. `interval_ms` is always\n 90|    /// (re)written from the caller's live config value, so a per-machine period\n 91|    /// change takes effect on the next start.\n...\n 96|        now_ms: u64,\n*97|    ) -> std::io::Result<Self> {\n 98|        let path = anchor_path(key);\n### deelevate.rs\n*1|//! De-elevation seam for daemon spawn (KNOWN-HAZARDS 5.7).\n 2|//!\n 3|//! Membership-implies-reachability made every `spt` invocation a potential\n*4|//! daemon spawner (`ensure_running`) — including the elevation-gated ones\n*5|//! (`subnet create`/`join`, REQ-SUBNET-4). A daemon spawned with the\n*6|//! elevated token is poison: on Windows its named pipes deny unelevated\n 7|//! clients (every later `spt` reads not-running → spawn → bind\n*8|//! Access-denied); on Linux a sudo'd spawn roots the daemon and — because\n 9|//! sudo flips `$HOME` — can mint a *different node identity* in root's\n 10|//! universe (the key-flip behind the 4.10 ghost rows).\n 11|//!\n*12|//! The invariant: **the daemon always runs unelevated in the invoking\n 13|//! user's universe, regardless of which command spawns it.** Two\n 14|//! enforcement points consume this module:\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 20|//! - The [`crate::daemon::Daemon::run`] entry guard catches a *directly*\n*21|//!   elevated `spt daemon` — Linux drops privileges in-process\n 22|//!   ([`SudoInvoker::drop_in_process`]) before touching any state; Windows\n*23|//!   respawns de-elevated and exits.\n 24|//!\n*25|//! When no unelevated identity exists to drop to (UAC disabled, genuine\n 26|//! root login, SYSTEM), callers run as-is with a loud warning — a\n*27|//! consistent universe, never a torn one. Elevated one-shot *clients*\n*28|//! talking downward to an unelevated daemon are fine; the daemon side is\n 29|//! the invariant.\n 30|//!\n*31|//! The OS probes mirror `spt`'s REQ-PAIR-6 `elevation.rs` (the CLI gate);\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 37|\n 38|#[cfg(unix)]\n 39|pub use unix::{daemon_target_user, sudo_invoker, SudoInvoker};\n...\n 41|#[cfg(windows)]\n*42|pub use windows::{has_deelevation_target, is_elevated, spawn_deelevated};\n 43|\n*44|/// What a spawner should do about elevation — the pure decision seam (the\n 45|/// REQ-PAIR-6 `gate_for` posture: the OS probe is environment-dependent,\n*46|/// the decision is unit-tested both ways).\n 47|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n 48|pub enum SpawnPlan {\n### digest.rs\n 1|//! The session-digest **log projection** — the daemon-side, on-demand projection\n*2|//! of an endpoint's live activity buffer from its normalized session logs\n 3|//! (ADR-0008 amendment, 2026-06-12; REQ-TERM-4 log-source re-foundation).\n 4|//!\n*5|//! ## On-demand projection, not a byte feed\n 6|//!\n*7|//! The superseded shape folded broker PTY bytes through an adapter-pattern parser\n*8|//! on every output chunk. The digest is now a **projection of the same normalized\n*9|//! history the echo-commune consumes** — so there is nothing to feed: a snapshot\n 10|//! pull (or a `digest-entry` push) **projects on demand** from the endpoint's\n 11|//! records via [`project_endpoint_digest`], and the hub publishes the result.\n*12|//! Render surfaces read the PTY (the raw byte ring, unchanged); content surfaces\n 13|//! read the logs.\n 14|//!\n 15|//! ## Sources (one per endpoint)\n...\n 17|//! - An adapter that declares `[history]` (Path A locate+normalize / fetcher, or\n*18|//!   Path B native store) → the projection tails those normalized records. This\n*19|//!   is the **topology-independent** path: a Claude-Code-hosted endpoint's\n*20|//!   per-session JSONL is Path A's worked example.\n 21|//! - A **log-less** adapter pushes digest-records directly via `api digest-entry`\n 22|//!   into the perch's `digest.log` → the projection reads that.\n 23|//!\n*24|//! The records carry the published digest-record contract\n*25|//! ([`spt_term::DigestRecord`]); the projection folds them into the structured\n*26|//! [`Digest`] (N-turn window, collapsed tool sprints, ~25-char arg truncation).\n 27|//!\n*28|//! ## Snapshot vs delta\n 29|//!\n*30|//! A projection is diffed against the hub's prior snapshot\n 31|//! ([`crate::digesthub::DigestHub::project_and_publish`]) to produce a\n*32|//! [`DigestUpdate`]: `from` is where the changed turns begin, and the subscriber\n 33|//! applies it by **truncating its view to `from`, then appending `turns`**. The\n*34|//! delta-stream **contract** ships; its deltas are driven by pull / digest-entry\n*35|//! publishes (autonomous file-watch freshness is deferred to the consuming\n 36|//! frontend milestone — CONTEXT.md \"session digest\").\n 37|// [impl->REQ-TERM-4]\n 38|\n...\n 45|use spt_runtime::manifest::Digest as ManifestDigest;\n*46|use spt_store::perch::{self, ParentHint};\n*47|use spt_term::{Digest, DigestConfig, TimelineItem, Turn};\n 48|\n 49|/// Bound for the projection's extractor run (the `[digest]` extractor is a\n*50|/// subprocess — REQ-HAZARD-SUBPROCESS-TIMEOUT; a hang is killed, never blocks).\n 51|const DIGEST_FETCH_TIMEOUT: Duration = Duration::from_secs(30);\n 52|\n### digesthub.rs\n*1|//! The daemon-held digest hub + its control channel (ADR-0008 amendment /\n 2|//! REQ-TERM-4).\n 3|//!\n*4|//! The hub holds the latest projected snapshot per endpoint and fans every change\n*5|//! out to live subscribers. The digest is **projected on demand** from the\n*6|//! endpoint's normalized records ([`project_endpoint_digest`]) — there is no live\n*7|//! feed; a pull or a `digest-entry` push triggers a projection, and\n 8|//! [`DigestHub::project_and_publish`] diffs it against the prior snapshot to\n*9|//! produce the [`DigestUpdate`] delta. The control channel is the access surface\n 10|//! ADR-0008 describes — two modes over one transport:\n 11|//!","truncated":true,"truncatedBy":"bytes","totalLines":989,"totalBytes":56388,"outputLines":899,"outputBytes":51176,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":989,"totalBytes":56388,"outputLines":899,"outputBytes":51176,"shownRange":{"start":1,"end":899},"nextOffset":900}}},"isError":false,"timestamp":1784365474713}}
{"type":"message","id":"089e781c","parentId":"d66e5aea","timestamp":"2026-07-18T09:04:43.354Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Locating spt-term crate specifically**"},{"type":"toolCall","id":"call_ZPrI5QgSaErXC24ZnnunegCL|fc_0ce5441d29b9f05d016a5b41ab2c0c819baf11db8bb72289f3","name":"read","arguments":{"i":"Reading domain terminology","path":"CONTEXT.md","selector":"1-260"}},{"type":"toolCall","id":"call_RlgSTgASgC1L5at94ovS8EKn|fc_0ce5441d29b9f05d016a5b41ab2c28819b8933a06a00f71f37","name":"read","arguments":{"i":"Reading screen grid ADR","path":"docs/adr/0031-server-side-screen-grid-render-repaint.md","selector":"1-260"}},{"type":"toolCall","id":"call_U0ZdOtiYAavmMntWKsAMkN2B|fc_0ce5441d29b9f05d016a5b41ab2c80819b9f0bfc4005cebfec","name":"grep","arguments":{"i":"Finding PTY hazard references","pattern":"SCREEN-GRID|screen grid|whitespace|blank row|stale character|clear sequence|repaint|terminal restore|disconnect timeout","path":"docs/KNOWN-HAZARDS.md; docs; crates","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_WwtfrRW5XeIquttKQU1TQhIN|fc_0ce5441d29b9f05d016a5b41ab2c8c819b9d1ae56da5cd9775","name":"glob","arguments":{"i":"Mapping terminal crate","path":"crates/spt-term/**/*.rs; crates/spt-term/Cargo.toml; crates/spt-proto/**/*.rs","hidden":true,"gitignore":true,"limit":300}},{"type":"toolCall","id":"call_Z4WmtdxlRCZvdxqOHAwSQRWS|fc_0ce5441d29b9f05d016a5b41ab2c94819bb19c8ded681bdef9","name":"read","arguments":{"i":"Reading RC implementation","path":"crates/spt/src/rc.rs","selector":"1-300"}},{"type":"toolCall","id":"call_y3MKcLPOpl9Vv0HqFkd83jvZ|fc_0ce5441d29b9f05d016a5b41ab2c9c819b84fd6572838da68a","name":"read","arguments":{"i":"Reading broker grid core","path":"crates/spt-daemon/src/broker.rs","selector":"620-1100"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":42608,"output":307,"cacheRead":14848,"cacheWrite":0,"totalTokens":57763,"reasoningTokens":15,"cost":{"input":0.21304,"output":0.00921,"cacheRead":0.007424,"cacheWrite":0,"total":0.229674}},"stopReason":"toolUse","timestamp":1784365474758,"responseId":"resp_0ce5441d29b9f05d016a5b41a31b00819ba820716adc375c0c","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b41a51b30819b85da3f99b69a270d","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0Gr8Rp1vL9nDiVSHoC0Y4KH8l1lQL18h1wj77zwsIOxrhZ8c-jkOiL2YG-Vr-WBG_OWWZhoPO8iayQCrh1b846RFhJpg-aKneCBz2cJOU5NJfa8u-y6ouvgZg7bhlhA3_QKVJU50RSRF7wNa7R3jad_HVpZMQ_HU8wh2_IL3Vd7PrQzZHKQVFLxwGNG2nhy_BmoYpb8HqNgmUrPuvO-EsuuhzFhTYvq6sFwDEbXT-v2ZlipshqpGSP2ZY0KWhqG4MEds7Cbet4lQA5LfWG3fNTBr801Kp-R2kbaOoEwZYkp5szFe2LpAqeqy_MX-GKeKpvjew39kpImOOWcncUS165DcTVUc9GkqlaQA11aYord-0zmdzOUMV5uO0ppX2iXvrt555i0X2XZbxWQO2_C7UJIYzd-LkZPxc4LVeFOGiXySlgUoi9dwnrxEU721T17cImJ1_bDw5bxlu8t5gZ3qFch8-ffBT8WlLUcYRwG7ZvXJwu2B4SNeDOZVgNAiKjxp3OIaE__TtV6QpiZB7dFAG4X8NnqpZ1ycyGIdel_ev_IAXUiNYDE6JdpKVVnp9qOgob_ht6eec9twxHeUG1c-I1Jlii88zL2V-JP_oPst21Y89Uu8eNOoV8VwXPtqU-hYqNRDzRjKlNADCyngTzcoq1mI8tm1ct_CxX7U43WOkWcQLqL6Qnns62IraCSvpHYhFx3qmWQ3g9HeBZgOK1TR9bAuabXEXie3zo0jMj1y3MGupZKKVvoLXEAdYw5KXbNx_h5Tp7Jxa6Kl_Z5BM7rmaW2DWpkcrXK8kMHHoK38J5OH3vbkURcV9fupw-towQbJtfmdPZ1Y2Emh5mHJzjeVa59r4VN-1C85v69hkcpcLg6AOvtQGxw1zdkDIZ8Vex_AqCmYLHsG_ArXMSQKRr-k-Qx8zbrnZphoS0ziFkkTZwtL-A7J2vtD1taUflxm_5Nr2h3mULEwQ5a_xQjd0_ah1-q0GNwdXVnTBqFGIC7KT2P6RE=","summary":[{"type":"summary_text","text":"**Locating spt-term crate specifically**"}]},{"id":"fc_0ce5441d29b9f05d016a5b41ab2c0c819baf11db8bb72289f3","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading domain terminology\",\"path\":\"CONTEXT.md\",\"selector\":\"1-260\"}","call_id":"call_ZPrI5QgSaErXC24ZnnunegCL","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41ab2c28819b8933a06a00f71f37","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading screen grid ADR\",\"path\":\"docs/adr/0031-server-side-screen-grid-render-repaint.md\",\"selector\":\"1-260\"}","call_id":"call_RlgSTgASgC1L5at94ovS8EKn","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41ab2c80819b9f0bfc4005cebfec","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding PTY hazard references\",\"pattern\":\"SCREEN-GRID|screen grid|whitespace|blank row|stale character|clear sequence|repaint|terminal restore|disconnect timeout\",\"path\":\"docs/KNOWN-HAZARDS.md; docs; crates\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_U0ZdOtiYAavmMntWKsAMkN2B","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41ab2c8c819b9d1ae56da5cd9775","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping terminal crate\",\"path\":\"crates/spt-term/**/*.rs; crates/spt-term/Cargo.toml; crates/spt-proto/**/*.rs\",\"hidden\":true,\"gitignore\":true,\"limit\":300}","call_id":"call_WwtfrRW5XeIquttKQU1TQhIN","name":"glob"},{"id":"fc_0ce5441d29b9f05d016a5b41ab2c94819bb19c8ded681bdef9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading RC implementation\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1-300\"}","call_id":"call_Z4WmtdxlRCZvdxqOHAwSQRWS","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41ab2c9c819b84fd6572838da68a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker grid core\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"620-1100\"}","call_id":"call_y3MKcLPOpl9Vv0HqFkd83jvZ","name":"read"}]},"duration":8587.658800005913,"ttft":2342.4122000038624,"contextSnapshot":{"promptTokens":57456,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZPrI5QgSaErXC24ZnnunegCL|fc_0ce5441d29b9f05d016a5b41ab2c0c819baf11db8bb72289f3","toolName":"read","startedAt":"2026-07-18T09:04:43.354Z","args":{"path":"CONTEXT.md"},"intent":"Reading domain terminology"},"id":"77824fea","parentId":"089e781c","timestamp":"2026-07-18T09:04:43.354Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_RlgSTgASgC1L5at94ovS8EKn|fc_0ce5441d29b9f05d016a5b41ab2c28819b8933a06a00f71f37","toolName":"read","startedAt":"2026-07-18T09:04:43.354Z","args":{"path":"docs/adr/0031-server-side-screen-grid-render-repaint.md"},"intent":"Reading screen grid ADR"},"id":"711cf5bb","parentId":"77824fea","timestamp":"2026-07-18T09:04:43.354Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_U0ZdOtiYAavmMntWKsAMkN2B|fc_0ce5441d29b9f05d016a5b41ab2c80819b9f0bfc4005cebfec","toolName":"grep","startedAt":"2026-07-18T09:04:43.354Z","args":{"path":"docs/KNOWN-HAZARDS.md; docs; crates"},"intent":"Finding PTY hazard references"},"id":"fabe1aaf","parentId":"711cf5bb","timestamp":"2026-07-18T09:04:43.354Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_WwtfrRW5XeIquttKQU1TQhIN|fc_0ce5441d29b9f05d016a5b41ab2c8c819b9d1ae56da5cd9775","toolName":"glob","startedAt":"2026-07-18T09:04:43.354Z","args":{"path":"crates/spt-term/**/*.rs; crates/spt-term/Cargo.toml; crates/spt-proto/**/*.rs"},"intent":"Mapping terminal crate"},"id":"9064b92c","parentId":"fabe1aaf","timestamp":"2026-07-18T09:04:43.354Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Z4WmtdxlRCZvdxqOHAwSQRWS|fc_0ce5441d29b9f05d016a5b41ab2c94819bb19c8ded681bdef9","toolName":"read","startedAt":"2026-07-18T09:04:43.354Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading RC implementation"},"id":"3e6a822b","parentId":"9064b92c","timestamp":"2026-07-18T09:04:43.354Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_y3MKcLPOpl9Vv0HqFkd83jvZ|fc_0ce5441d29b9f05d016a5b41ab2c9c819b84fd6572838da68a","toolName":"read","startedAt":"2026-07-18T09:04:43.354Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker grid core"},"id":"f5fb2aee","parentId":"3e6a822b","timestamp":"2026-07-18T09:04:43.354Z"}
{"type":"message","id":"a8d13c1d","parentId":"f5fb2aee","timestamp":"2026-07-18T09:04:43.358Z","message":{"role":"toolResult","toolCallId":"call_WwtfrRW5XeIquttKQU1TQhIN|fc_0ce5441d29b9f05d016a5b41ab2c8c819b9d1ae56da5cd9775","toolName":"glob","content":[{"type":"text","text":"# crates/\n## spt-term/\nCargo.toml\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"}],"details":{"scopePath":"crates/spt-term/**/*.rs, crates/spt-term/Cargo.toml, crates/spt-proto/**/*.rs","fileCount":25,"files":["crates/spt-term/src/screen.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-term/Cargo.toml","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"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"},"isError":false,"timestamp":1784365483358}}
{"type":"message","id":"072cad25","parentId":"a8d13c1d","timestamp":"2026-07-18T09:04:43.363Z","message":{"role":"toolResult","toolCallId":"call_RlgSTgASgC1L5at94ovS8EKn|fc_0ce5441d29b9f05d016a5b41ab2c28819b8933a06a00f71f37","toolName":"read","content":[{"type":"text","text":"# Server-side screen grid + clean repaint on attach\n\n## Status\n\naccepted (2026-06-30) · companion to `V0.19.0-P6-SCREEN-GRID-DESIGN.md` · relates to [ADR-0008](0008-live-activity-buffer-pty-digest.md) (render-vs-content split) and [ADR-0001](0001-clean-room-fork-from-claude-skill-owl.md) (clean-room provenance).\n\n## Context\n\nA cold attach streams the entire retained **raw-byte ring** as its initial batch (`become_controller`, broker.rs:402-416). For an alt-screen TUI (Claude Code) that ring is a mid-stream rendering protocol; replayed raw into a fresh terminal it corrupts the screen + scrollback (field bugs #6/#7/#8). We need a server-side render model that interprets the byte stream into an authoritative current screen and synthesizes one clean repaint at attach.\n\nTwo questions this ADR settles:\n1. Does a server-side PTY-byte interpreter **contradict ADR-0008**, which retired the PTY-byte parser?\n2. May we take a **third-party VT parser** dependency under ADR-0001 (clean-room everything except the sister project's stable wire/schema formats)?\n\n## Decision\n\n<!-- [doc->REQ-BROKER-SCREEN-GRID] the server-side render grid + clean repaint on attach:\n     a clean-room `ScreenGrid` (vte::Perform) interprets the byte stream into an\n     authoritative current screen; `become_controller`/`add_viewer` emit one synthesized\n     repaint instead of the raw ring. Companion design: V0.19.0-P6-SCREEN-GRID-DESIGN.md. -->\n\n**1. The render grid does NOT reverse ADR-0008.** ADR-0008's governing split (2026-06-12 amendment) is: **render surfaces read the PTY; content surfaces read the logs.** It retired the PTY-byte parser only for **content** — the digest, which now projects from normalized session logs — because extracting *semantic turn content* from a repaint-heavy byte stream is fragile and harness-specific. The screen grid is a **render** surface: it reconstructs the *visible screen*, not conversation content. It is the render-reads-PTY side ADR-0008 explicitly preserved, and it resolves the exact corruption ADR-0008 named (\"alt-screen PTY = rendering protocol; repaints corrupt it\"). The content parser stays retired. This ADR records the distinction so a future reader does …\n\n**2. Take a vetted VT-parser dependency (`vte`), clean-room the grid.** The byte→action layer is `vte` (the alacritty terminal parser state machine): small, single-purpose, battle-tested against real-world escape sequences. The `ScreenGrid` (cells, cursor, main/alt buffer, modes) implementing `vte::Perform` is clean-room. Rationale under ADR-0001: ADR-0001 defaults to clean-room, but its intent is to avoid copying the sister project's evolving logic and to keep provenance clean — a **universal, standardized, spec-fragile-at-the-edges VT state machine** is neither sister-derived nor a place where re-implementation adds value; a hand-rolled parser's edge-sequence bugs would produce precisely the rendering artifacts #6 exists to eliminate. The dependency is nar…\n\n**Scope:** current screen only (viewport + cursor + alt/main). Scrollback stays the raw ring (history). The grid drives only the attach repaint. Wire format unchanged (`AttachRecord::Output`); only the initial-batch *content* changes. See the design doc for architecture, hazard integration, and the build plan.\n\n**Amendment (2026-07-06, REMOTE-TRUTH E-2):** the repaint also carries the child's **last-set window title** (OSC 0/2, captured on the grid, cleared by RIS) — a cold attach previously painted a perfect screen under the attacher's stale title because `vte`'s default drops OSC.\n\n## Consequences\n\n- New first-party dependency `vte`; folded into the release Cargo.lock first-party-name discipline (a version pin + audit at cut).\n- The grid is the **OS-neutral render-replay layer** CONTEXT §terminal anticipated (forkpty raw-pipe vs ConPTY repaint-on-resize both feed one grid); it slots behind `SessionSurface`/`OutputStream`.\n- `become_controller`/`add_viewer` emit a synthesized clean repaint as the initial batch; live frames after attach are unchanged raw bytes. Cross-node attach (#4) and any client are agnostic.\n- Must preserve the attach/serve hazard invariants (REQ-HAZARD-INJECT-CONTROL-COEXIST 7.12, CONTROLLER-WRITER-REORDER 7.21, VIEWER-ISOLATION 7.7 + skip-to-live, the wedge family) — the grid is on the output path only, touches neither the input/effect journal nor the QUIC deadlines.\n- Unlocks the follow-up sticky ID-marker redo (#8/#14 marker-half) as a grid-backed per-frame overlay (separate REQ).\n- Composes with #12 (REQ-RC-WIN-VT-OUTPUT): the repaint is ANSI bytes; on Win10 conhost it renders only once VT-output-processing is enabled client-side (#12).\n\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"# Server-side screen grid + clean repaint on attach\r\n\r\n## Status\r\n\r\naccepted (2026-06-30) · companion to `V0.19.0-P6-SCREEN-GRID-DESIGN.md` · relates to [ADR-0008](0008-live-activity-buffer-pty-digest.md) (render-vs-content split) and [ADR-0001](0001-clean-room-fork-from-claude-skill-owl.md) (clean-room provenance).\r\n\r\n## Context\r\n\r\nA cold attach streams the entire retained **raw-byte ring** as its initial batch (`become_controller`, broker.rs:402-416). For an alt-screen TUI (Claude Code) that ring is a mid-stream rendering protocol; replayed raw into a fresh terminal it corrupts the screen + scrollback (field bugs #6/#7/#8). We need a server-side render model that interprets the byte stream into an authoritative current screen and synthesizes one clean repaint at attach.\r\n\r\nTwo questions this ADR settles:\r\n1. Does a server-side PTY-byte interpreter **contradict ADR-0008**, which retired the PTY-byte parser?\r\n2. May we take a **third-party VT parser** dependency under ADR-0001 (clean-room everything except the sister project's stable wire/schema formats)?\r\n\r\n## Decision\r\n\r\n<!-- [doc->REQ-BROKER-SCREEN-GRID] the server-side render grid + clean repaint on attach:\r\n     a clean-room `ScreenGrid` (vte::Perform) interprets the byte stream into an\r\n     authoritative current screen; `become_controller`/`add_viewer` emit one synthesized\r\n     repaint instead of the raw ring. Companion design: V0.19.0-P6-SCREEN-GRID-DESIGN.md. -->\r\n\r\n**1. The render grid does NOT reverse ADR-0008.** ADR-0008's governing split (2026-06-12 amendment) is: **render surfaces read the PTY; content surfaces read the logs.** It retired the PTY-byte parser only for **content** — the digest, which now projects from normalized session logs — because extracting *semantic turn content* from a repaint-heavy byte stream is fragile and harness-specific. The screen grid is a **render** surface: it reconstructs the *visible screen*, not conversation content. It is the render-reads-PTY side ADR-0008 explicitly preserved, and it resolves the exact corruption ADR-0008 named (\"alt-screen PTY = rendering protocol; repaints corrupt it\"). The content parser stays retired. This ADR records the distinction so a future reader does …\n\r\n**2. Take a vetted VT-parser dependency (`vte`), clean-room the grid.** The byte→action layer is `vte` (the alacritty terminal parser state machine): small, single-purpose, battle-tested against real-world escape sequences. The `ScreenGrid` (cells, cursor, main/alt buffer, modes) implementing `vte::Perform` is clean-room. Rationale under ADR-0001: ADR-0001 defaults to clean-room, but its intent is to avoid copying the sister project's evolving logic and to keep provenance clean — a **universal, standardized, spec-fragile-at-the-edges VT state machine** is neither sister-derived nor a place where re-implementation adds value; a hand-rolled parser's edge-sequence bugs would produce precisely the rendering artifacts #6 exists to eliminate. The dependency is nar…\n\r\n**Scope:** current screen only (viewport + cursor + alt/main). Scrollback stays the raw ring (history). The grid drives only the attach repaint. Wire format unchanged (`AttachRecord::Output`); only the initial-batch *content* changes. See the design doc for architecture, hazard integration, and the build plan.\r\n\r\n**Amendment (2026-07-06, REMOTE-TRUTH E-2):** the repaint also carries the child's **last-set window title** (OSC 0/2, captured on the grid, cleared by RIS) — a cold attach previously painted a perfect screen under the attacher's stale title because `vte`'s default drops OSC.\r\n\r\n## Consequences\r\n\r\n- New first-party dependency `vte`; folded into the release Cargo.lock first-party-name discipline (a version pin + audit at cut).\r\n- The grid is the **OS-neutral render-replay layer** CONTEXT §terminal anticipated (forkpty raw-pipe vs ConPTY repaint-on-resize both feed one grid); it slots behind `SessionSurface`/`OutputStream`.\r\n- `become_controller`/`add_viewer` emit a synthesized clean repaint as the initial batch; live frames after attach are unchanged raw bytes. Cross-node attach (#4) and any client are agnostic.\r\n- Must preserve the attach/serve hazard invariants (REQ-HAZARD-INJECT-CONTROL-COEXIST 7.12, CONTROLLER-WRITER-REORDER 7.21, VIEWER-ISOLATION 7.7 + skip-to-live, the wedge family) — the grid is on the output path only, touches neither the input/effect journal nor the QUIC deadlines.\r\n- Unlocks the follow-up sticky ID-marker redo (#8/#14 marker-half) as a grid-backed per-frame overlay (separate REQ).\r\n- Composes with #12 (REQ-RC-WIN-VT-OUTPUT): the repaint is ANSI bytes; on Win10 conhost it renders only once VT-output-processing is enabled client-side (#12).\r\n","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0031-server-side-screen-grid-render-repaint.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784365483363}}
{"type":"message","id":"ab91ceba","parentId":"072cad25","timestamp":"2026-07-18T09:04:43.383Z","message":{"role":"toolResult","toolCallId":"call_Z4WmtdxlRCZvdxqOHAwSQRWS|fc_0ce5441d29b9f05d016a5b41ab2c94819bb19c8ded681bdef9","toolName":"read","content":[{"type":"text","text":"//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\n//! local terminal to a broker-held PTY session.\n//!\n//! **One pump, swappable transport** (the doyle invariant). The attach protocol\n//! is `serve_attach` on the target side ⇄ this pump on the operator side, both\n//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the\n//! degenerate single-node case: the operator dials its OWN node (loopback peer)\n//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`\n//! against the same broker — the exact cross-node accept+serve path, no local\n//! special-case branch. The one-path invariant lives at this pump, not the\n//! transport under the stream.\n//!\n//! **Detach never kills the session.** The broker owns the PTY; closing our send\n//! side ends the viewport with the session still running (`serve_attach` →\n//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\n//! attach-lifecycle invariant).\n//!\n//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\n//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel\n//! (and watches for the detach keybind); the main loop interleaves draining that\n//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.\n// [impl->REQ-RC-1]\n\nuse std::io::{Read, Write};\nuse std::sync::atomic::{AtomicU64, Ordering};\nuse std::sync::mpsc;\nuse std::time::{Duration, Instant};\n\nuse spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\nuse spt_daemon::effect::{Minter, MintedOp};\nuse spt_daemon::brain::{now_ms, Brain, BrokerEvent};\nuse spt_daemon::msg::decode_bytes;\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\n\n/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\n/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\n/// literal ctrl-b to the session; any other byte after the prefix is sent\n/// through verbatim (prefix + byte). Documented in the CLI help.\nconst DETACH_PREFIX: u8 = 0x02; // ctrl-b\nconst DETACH_KEY: u8 = b'd';\n\n/// How long the main loop blocks for an inbound event before looping back to\n/// drain stdin — the full-duplex interleave granularity.\nconst POLL_SLICE: Duration = Duration::from_millis(40);\n\n/// A message from the stdin reader thread to the pump.\nenum StdinMsg {\n    /// Raw bytes to forward to the session as `AttachRecord::Input`.\n    Bytes(Vec<u8>),\n    /// The operator hit the detach keybind — end the viewport, leave the session.\n    Detach,\n}\n\n/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\n/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for\n/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel\n/// events to us instead of scrolling its own buffer — so to keep scroll working we\n/// must FORWARD wheel events to the harness, but ONLY when the harness has asked\n/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\n/// (scanning output); the Windows stdin reader READS them to decide whether to\n/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on\n/// all platforms (the scan is cheap + keeps the model correct); only the Windows\n/// reader acts on it (Unix terminals scroll natively, no capture).\n#[derive(Default)]\nstruct MouseMode {\n    /// The harness requested mouse reporting (DECSET 1000 / 1002 / 1003).\n    enabled: std::sync::atomic::AtomicBool,\n    /// The harness requested SGR extended coordinates (DECSET 1006). We forward a\n    /// scroll only when this is on (the modern encoding the harness will parse).\n    sgr: std::sync::atomic::AtomicBool,\n}\n\n/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset\n/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +\n/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT\n/// across two output chunks (the harness's mouse-enable can land across a decoder\n/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each\n/// `n` in {1000,1002,1003} sets `enabled = (final=='h')`, for 1006 sets `sgr`.\n#[derive(Default)]\nstruct MouseModeScanner {\n    /// A partial `ESC[?…` left dangling at a chunk boundary, prepended next feed.\n    carry: Vec<u8>,\n}\n\n/// The longest incomplete private-mode prefix we keep across a chunk boundary. A\n/// longer dangling `ESC[?…` with no final `h`/`l` is not a mode-set we care about\n/// (runaway) — dropped rather than buffered unboundedly.\nconst MOUSE_DECSET_CARRY_MAX: usize = 32;\n\nimpl MouseModeScanner {\n    /// Feed one output chunk; apply any complete DECSET mouse-mode toggles to\n    /// `mode`, carrying a trailing partial sequence to the next call.\n    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {\n        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.\n        if self.carry.is_empty() && !chunk.contains(&0x1b) {\n            return;\n        }\n        let mut buf = std::mem::take(&mut self.carry);\n        buf.extend_from_slice(chunk);\n        let mut i = 0;\n        while i < buf.len() {\n            if buf[i] != 0x1b {\n                i += 1;\n                continue;\n            }\n            match parse_decset_private(&buf[i..]) {\n                DecsetParse::Complete { consumed, params, set } => {\n                    for p in params {\n                        apply_mouse_mode(mode, p, set);\n                    }\n                    i += consumed;\n                }\n                // An incomplete `ESC[?…` at the tail: carry it (bounded) for the\n                // next chunk; a too-long dangling run is treated as not-a-DECSET.\n                DecsetParse::Incomplete => {\n                    let tail = &buf[i..];\n                    if tail.len() <= MOUSE_DECSET_CARRY_MAX {\n                        self.carry = tail.to_vec();\n                    }\n                    return;\n                }\n                // Not a private-mode sequence — skip this ESC, keep scanning.\n                DecsetParse::NotDecset => i += 1,\n            }\n        }\n    }\n}\n\n/// Result of trying to parse a private-mode set/reset at the start of `s` (which\n/// begins with `ESC`).\nenum DecsetParse {\n    /// A complete `ESC[?<params>h|l`: the byte length consumed, the numeric\n    /// params, and `set` (`true` for `h`, `false` for `l`).\n    Complete { consumed: usize, params: Vec<u16>, set: bool },\n    /// `ESC[?…` with no final `h`/`l` yet (chunk boundary) — carry + retry.\n    Incomplete,\n    /// `s` does not start a private-mode sequence (`ESC` of something else).\n    NotDecset,\n}\n\n/// Parse a single `ESC[?<digits>[;<digits>…]h|l` private-mode sequence at the\n/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.\nfn parse_decset_private(s: &[u8]) -> DecsetParse {\n    // Need at least `ESC [ ?`.\n    if s.len() < 3 {\n        // Could be the very start of one split across the boundary.\n        if s.first() == Some(&0x1b) && s.get(1).is_none_or(|&b| b == b'[')\n            && s.get(2).is_none_or(|&b| b == b'?')\n        {\n            return DecsetParse::Incomplete;\n        }\n        return DecsetParse::NotDecset;\n    }\n    if s[1] != b'[' || s[2] != b'?' {\n        return DecsetParse::NotDecset;\n    }\n    let mut params: Vec<u16> = Vec::new();\n    let mut cur: Option<u32> = None;\n    let mut j = 3;\n    while j < s.len() {\n        match s[j] {\n            b'0'..=b'9' => {\n                let d = (s[j] - b'0') as u32;\n                cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));\n            }\n            b';' => {\n                params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);\n                cur = None;\n            }\n            b'h' | b'l' => {\n                if let Some(v) = cur {\n                    params.push(v.min(u16::MAX as u32) as u16);\n                }\n                return DecsetParse::Complete {\n                    consumed: j + 1,\n                    params,\n                    set: s[j] == b'h',\n                };\n            }\n            // Any other byte ⇒ this isn't a clean private-mode set/reset.\n            _ => return DecsetParse::NotDecset,\n        }\n        j += 1;\n    }\n    // Ran out before a final `h`/`l` ⇒ split across the chunk boundary.\n    DecsetParse::Incomplete\n}\n\n/// Apply one DECSET private-mode toggle to the tracked [`MouseMode`].\nfn apply_mouse_mode(mode: &MouseMode, param: u16, set: bool) {\n    use std::sync::atomic::Ordering::Release;\n    match param {\n        1000 | 1002 | 1003 => mode.enabled.store(set, Release),\n        1006 => mode.sgr.store(set, Release),\n        _ => {}\n    }\n}\n\n/// Bug #14 (REQ-RC-IDMARKER-DISABLE): ship the top-right endpoint-id status\n/// marker DISABLED. As a one-shot absolutely-positioned paint it scrolls off\n/// screen and is never re-stickied (#14), and its DECSC/clear/SGR injection\n/// splices into the harness's in-flight drawing, leaving residual artifacts on\n/// animate/scroll/resize (#7/#8). Operator ruling: OFF for now — revisit as a\n/// proper per-frame sticky overlay only once REQ-BROKER-SCREEN-GRID provides a\n/// server-side screen model (the concept is saved for a future web SPT GUI).\n/// Flip to `true` to restore the v0.16.0 W6 behavior (the StatusRow byte-emit\n/// machinery + its unit tests are kept intact, only the wiring is gated).\nconst STATUS_ROW_ENABLED: bool = false;\n\n/// Whether the controller reserved-row identity marker is active for this attach:\n/// only for a controller (a viewer's viewport is read-only) AND only while the\n/// marker is enabled (bug #14 ships it off).\n// [impl->REQ-RC-IDMARKER-DISABLE]\nfn status_row_active(view: bool) -> bool {\n    !view && STATUS_ROW_ENABLED\n}\n\n/// The CONTROLLER's reserved TOP status row (v0.16.0 W6, REQ-RC-IDENTITY): a\n/// one-line identity banner — `{subnet} : {endpoint_id} @ {node}` — painted on the\n/// real terminal's row 1, right-aligned + cyan, while the harness PTY runs in a\n/// scroll region reserving that row (DECSTBM). It tells the operator WHICH endpoint\n/// on WHICH node they are driving, so a multi-attach operator never types into the\n/// wrong session. Pure byte-emit (no I/O) so the exact VT shapes unit-test\n/// directly; the pump writes the bytes. Tracks the FULL terminal `rows` (it paints\n/// row 1 on the real terminal) while the PTY is told it has `rows - 1`.\n// [impl->REQ-RC-IDENTITY]\nstruct StatusRow {\n    /// The resolved identity line (ASCII status text, right-aligned on paint).\n    text: String,\n    /// The REAL terminal row count (the status owns row 1; the PTY gets rows-1).\n    rows: u16,\n    /// The terminal column count (the right-align anchor).\n    cols: u16,\n}\n\nimpl StatusRow {\n    /// Set the DECSTBM scroll region to reserve row 1, then paint the banner.\n    /// `ESC[2;{rows}r` makes the harness scroll within rows 2..=rows (the harness\n    /// never touches row 1), then [`repaint_bytes`](Self::repaint_bytes) draws it.\n    // [impl->REQ-RC-IDENTITY]\n    fn assert_bytes(&self) -> Vec<u8> {\n        let mut out = Vec::new();\n        // Reserve the top row: top margin = line 2, bottom margin = rows.\n        out.extend_from_slice(format!(\"\\x1b[2;{}r\", self.rows).as_bytes());\n        out.extend_from_slice(&self.repaint_bytes());\n        out\n    }\n\n    /// Repaint row 1 WITHOUT disturbing the harness cursor: save cursor (DECSC),\n    /// home to row 1, clear the line, jump to the right-aligned start column, set\n    /// cyan, write the text, reset SGR, restore cursor (DECRC). The right-align\n    /// start column is `cols - width + 1` clamped to ≥ 1 (text wider than the\n    /// terminal starts at col 1 and is allowed to fill the row).\n    // [impl->REQ-RC-IDENTITY]\n    fn repaint_bytes(&self) -> Vec<u8> {\n        let width = self.display_width();\n        // Right-align: 1-based start column, clamped to col 1 when text ≥ cols.\n        let col = (self.cols as usize)\n            .saturating_sub(width)\n            .saturating_add(1)\n            .max(1);\n        let mut out = Vec::new();\n        out.extend_from_slice(b\"\\x1b7\"); // DECSC: save cursor\n        out.extend_from_slice(b\"\\x1b[1;1H\"); // home to row 1, col 1\n        out.extend_from_slice(b\"\\x1b[2K\"); // clear the whole line\n        out.extend_from_slice(format!(\"\\x1b[1;{col}H\").as_bytes()); // right-align start\n        out.extend_from_slice(b\"\\x1b[36m\"); // cyan\n        out.extend_from_slice(self.text.as_bytes());\n        out.extend_from_slice(b\"\\x1b[0m\"); // reset SGR\n        out.extend_from_slice(b\"\\x1b8\"); // DECRC: restore cursor\n        out\n    }\n\n    /// Tear the status down on detach: reset the scroll region to the full screen\n    /// (`ESC[r`), home to row 1, clear it — so the terminal is clean afterward.\n    // [impl->REQ-RC-IDENTITY]\n    fn restore_bytes() -> Vec<u8> {\n        let mut out = Vec::new();\n        out.extend_from_slice(b\"\\x1b[r\"); // reset scroll region (full screen)\n        out.extend_from_slice(b\"\\x1b[1;1H\"); // home to row 1\n        out.extend_from_slice(b\"\\x1b[2K\"); // clear the reclaimed row\n        out\n    }\n\n    /// Update the tracked terminal size on a window-change (the pump re-asserts\n    /// the margin + repaints after).\n    // [impl->REQ-RC-IDENTITY]\n    fn update_size(&mut self, rows: u16, cols: u16) {\n        self.rows = rows;\n        self.cols = cols;\n    }\n\n    /// The on-screen width of the banner. ASCII status text, so a `chars` count is\n    /// the cell width (no wide-char / combining concerns here).\n    fn display_width(&self) -> usize {\n        self.text.chars().count()\n    }\n}\n\n/// Resolve the controller's identity banner text (REQ-RC-IDENTITY):\n/// `{subnet} : {endpoint_id} @ {node}` — the endpoint's HOME subnet (`local` when\n/// unset), the attach target, and THIS machine's hostname label. An empty/absent\n/// node label omits the ` @ {node}` tail rather than rendering a lie. Resolved ONCE\n\n[Showing lines 1-303 of 3242. Use :304 to continue]"}],"details":{"truncation":{"content":"//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\r\n//! local terminal to a broker-held PTY session.\r\n//!\r\n//! **One pump, swappable transport** (the doyle invariant). The attach protocol\r\n//! is `serve_attach` on the target side ⇄ this pump on the operator side, both\r\n//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the\r\n//! degenerate single-node case: the operator dials its OWN node (loopback peer)\r\n//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`\r\n//! against the same broker — the exact cross-node accept+serve path, no local\r\n//! special-case branch. The one-path invariant lives at this pump, not the\r\n//! transport under the stream.\r\n//!\r\n//! **Detach never kills the session.** The broker owns the PTY; closing our send\r\n//! side ends the viewport with the session still running (`serve_attach` →\r\n//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\r\n//! attach-lifecycle invariant).\r\n//!\r\n//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\r\n//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel\r\n//! (and watches for the detach keybind); the main loop interleaves draining that\r\n//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.\r\n// [impl->REQ-RC-1]\r\n\r\nuse std::io::{Read, Write};\r\nuse std::sync::atomic::{AtomicU64, Ordering};\r\nuse std::sync::mpsc;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\r\nuse spt_daemon::effect::{Minter, MintedOp};\r\nuse spt_daemon::brain::{now_ms, Brain, BrokerEvent};\r\nuse spt_daemon::msg::decode_bytes;\r\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\r\n\r\n/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\r\n/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\r\n/// literal ctrl-b to the session; any other byte after the prefix is sent\r\n/// through verbatim (prefix + byte). Documented in the CLI help.\r\nconst DETACH_PREFIX: u8 = 0x02; // ctrl-b\r\nconst DETACH_KEY: u8 = b'd';\r\n\r\n/// How long the main loop blocks for an inbound event before looping back to\r\n/// drain stdin — the full-duplex interleave granularity.\r\nconst POLL_SLICE: Duration = Duration::from_millis(40);\r\n\r\n/// A message from the stdin reader thread to the pump.\r\nenum StdinMsg {\r\n    /// Raw bytes to forward to the session as `AttachRecord::Input`.\r\n    Bytes(Vec<u8>),\r\n    /// The operator hit the detach keybind — end the viewport, leave the session.\r\n    Detach,\r\n}\r\n\r\n/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\r\n/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for\r\n/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel\r\n/// events to us instead of scrolling its own buffer — so to keep scroll working we\r\n/// must FORWARD wheel events to the harness, but ONLY when the harness has asked\r\n/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\r\n/// (scanning output); the Windows stdin reader READS them to decide whether to\r\n/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on\r\n/// all platforms (the scan is cheap + keeps the model correct); only the Windows\r\n/// reader acts on it (Unix terminals scroll natively, no capture).\r\n#[derive(Default)]\r\nstruct MouseMode {\r\n    /// The harness requested mouse reporting (DECSET 1000 / 1002 / 1003).\r\n    enabled: std::sync::atomic::AtomicBool,\r\n    /// The harness requested SGR extended coordinates (DECSET 1006). We forward a\r\n    /// scroll only when this is on (the modern encoding the harness will parse).\r\n    sgr: std::sync::atomic::AtomicBool,\r\n}\r\n\r\n/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset\r\n/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +\r\n/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT\r\n/// across two output chunks (the harness's mouse-enable can land across a decoder\r\n/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each\r\n/// `n` in {1000,1002,1003} sets `enabled = (final=='h')`, for 1006 sets `sgr`.\r\n#[derive(Default)]\r\nstruct MouseModeScanner {\r\n    /// A partial `ESC[?…` left dangling at a chunk boundary, prepended next feed.\r\n    carry: Vec<u8>,\r\n}\r\n\r\n/// The longest incomplete private-mode prefix we keep across a chunk boundary. A\r\n/// longer dangling `ESC[?…` with no final `h`/`l` is not a mode-set we care about\r\n/// (runaway) — dropped rather than buffered unboundedly.\r\nconst MOUSE_DECSET_CARRY_MAX: usize = 32;\r\n\r\nimpl MouseModeScanner {\r\n    /// Feed one output chunk; apply any complete DECSET mouse-mode toggles to\r\n    /// `mode`, carrying a trailing partial sequence to the next call.\r\n    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {\r\n        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.\r\n        if self.carry.is_empty() && !chunk.contains(&0x1b) {\r\n            return;\r\n        }\r\n        let mut buf = std::mem::take(&mut self.carry);\r\n        buf.extend_from_slice(chunk);\r\n        let mut i = 0;\r\n        while i < buf.len() {\r\n            if buf[i] != 0x1b {\r\n                i += 1;\r\n                continue;\r\n            }\r\n            match parse_decset_private(&buf[i..]) {\r\n                DecsetParse::Complete { consumed, params, set } => {\r\n                    for p in params {\r\n                        apply_mouse_mode(mode, p, set);\r\n                    }\r\n                    i += consumed;\r\n                }\r\n                // An incomplete `ESC[?…` at the tail: carry it (bounded) for the\r\n                // next chunk; a too-long dangling run is treated as not-a-DECSET.\r\n                DecsetParse::Incomplete => {\r\n                    let tail = &buf[i..];\r\n                    if tail.len() <= MOUSE_DECSET_CARRY_MAX {\r\n                        self.carry = tail.to_vec();\r\n                    }\r\n                    return;\r\n                }\r\n                // Not a private-mode sequence — skip this ESC, keep scanning.\r\n                DecsetParse::NotDecset => i += 1,\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Result of trying to parse a private-mode set/reset at the start of `s` (which\r\n/// begins with `ESC`).\r\nenum DecsetParse {\r\n    /// A complete `ESC[?<params>h|l`: the byte length consumed, the numeric\r\n    /// params, and `set` (`true` for `h`, `false` for `l`).\r\n    Complete { consumed: usize, params: Vec<u16>, set: bool },\r\n    /// `ESC[?…` with no final `h`/`l` yet (chunk boundary) — carry + retry.\r\n    Incomplete,\r\n    /// `s` does not start a private-mode sequence (`ESC` of something else).\r\n    NotDecset,\r\n}\r\n\r\n/// Parse a single `ESC[?<digits>[;<digits>…]h|l` private-mode sequence at the\r\n/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.\r\nfn parse_decset_private(s: &[u8]) -> DecsetParse {\r\n    // Need at least `ESC [ ?`.\r\n    if s.len() < 3 {\r\n        // Could be the very start of one split across the boundary.\r\n        if s.first() == Some(&0x1b) && s.get(1).is_none_or(|&b| b == b'[')\r\n            && s.get(2).is_none_or(|&b| b == b'?')\r\n        {\r\n            return DecsetParse::Incomplete;\r\n        }\r\n        return DecsetParse::NotDecset;\r\n    }\r\n    if s[1] != b'[' || s[2] != b'?' {\r\n        return DecsetParse::NotDecset;\r\n    }\r\n    let mut params: Vec<u16> = Vec::new();\r\n    let mut cur: Option<u32> = None;\r\n    let mut j = 3;\r\n    while j < s.len() {\r\n        match s[j] {\r\n            b'0'..=b'9' => {\r\n                let d = (s[j] - b'0') as u32;\r\n                cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));\r\n            }\r\n            b';' => {\r\n                params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);\r\n                cur = None;\r\n            }\r\n            b'h' | b'l' => {\r\n                if let Some(v) = cur {\r\n                    params.push(v.min(u16::MAX as u32) as u16);\r\n                }\r\n                return DecsetParse::Complete {\r\n                    consumed: j + 1,\r\n                    params,\r\n                    set: s[j] == b'h',\r\n                };\r\n            }\r\n            // Any other byte ⇒ this isn't a clean private-mode set/reset.\r\n            _ => return DecsetParse::NotDecset,\r\n        }\r\n        j += 1;\r\n    }\r\n    // Ran out before a final `h`/`l` ⇒ split across the chunk boundary.\r\n    DecsetParse::Incomplete\r\n}\r\n\r\n/// Apply one DECSET private-mode toggle to the tracked [`MouseMode`].\r\nfn apply_mouse_mode(mode: &MouseMode, param: u16, set: bool) {\r\n    use std::sync::atomic::Ordering::Release;\r\n    match param {\r\n        1000 | 1002 | 1003 => mode.enabled.store(set, Release),\r\n        1006 => mode.sgr.store(set, Release),\r\n        _ => {}\r\n    }\r\n}\r\n\r\n/// Bug #14 (REQ-RC-IDMARKER-DISABLE): ship the top-right endpoint-id status\r\n/// marker DISABLED. As a one-shot absolutely-positioned paint it scrolls off\r\n/// screen and is never re-stickied (#14), and its DECSC/clear/SGR injection\r\n/// splices into the harness's in-flight drawing, leaving residual artifacts on\r\n/// animate/scroll/resize (#7/#8). Operator ruling: OFF for now — revisit as a\r\n/// proper per-frame sticky overlay only once REQ-BROKER-SCREEN-GRID provides a\r\n/// server-side screen model (the concept is saved for a future web SPT GUI).\r\n/// Flip to `true` to restore the v0.16.0 W6 behavior (the StatusRow byte-emit\r\n/// machinery + its unit tests are kept intact, only the wiring is gated).\r\nconst STATUS_ROW_ENABLED: bool = false;\r\n\r\n/// Whether the controller reserved-row identity marker is active for this attach:\r\n/// only for a controller (a viewer's viewport is read-only) AND only while the\r\n/// marker is enabled (bug #14 ships it off).\r\n// [impl->REQ-RC-IDMARKER-DISABLE]\r\nfn status_row_active(view: bool) -> bool {\r\n    !view && STATUS_ROW_ENABLED\r\n}\r\n\r\n/// The CONTROLLER's reserved TOP status row (v0.16.0 W6, REQ-RC-IDENTITY): a\r\n/// one-line identity banner — `{subnet} : {endpoint_id} @ {node}` — painted on the\r\n/// real terminal's row 1, right-aligned + cyan, while the harness PTY runs in a\r\n/// scroll region reserving that row (DECSTBM). It tells the operator WHICH endpoint\r\n/// on WHICH node they are driving, so a multi-attach operator never types into the\r\n/// wrong session. Pure byte-emit (no I/O) so the exact VT shapes unit-test\r\n/// directly; the pump writes the bytes. Tracks the FULL terminal `rows` (it paints\r\n/// row 1 on the real terminal) while the PTY is told it has `rows - 1`.\r\n// [impl->REQ-RC-IDENTITY]\r\nstruct StatusRow {\r\n    /// The resolved identity line (ASCII status text, right-aligned on paint).\r\n    text: String,\r\n    /// The REAL terminal row count (the status owns row 1; the PTY gets rows-1).\r\n    rows: u16,\r\n    /// The terminal column count (the right-align anchor).\r\n    cols: u16,\r\n}\r\n\r\nimpl StatusRow {\r\n    /// Set the DECSTBM scroll region to reserve row 1, then paint the banner.\r\n    /// `ESC[2;{rows}r` makes the harness scroll within rows 2..=rows (the harness\r\n    /// never touches row 1), then [`repaint_bytes`](Self::repaint_bytes) draws it.\r\n    // [impl->REQ-RC-IDENTITY]\r\n    fn assert_bytes(&self) -> Vec<u8> {\r\n        let mut out = Vec::new();\r\n        // Reserve the top row: top margin = line 2, bottom margin = rows.\r\n        out.extend_from_slice(format!(\"\\x1b[2;{}r\", self.rows).as_bytes());\r\n        out.extend_from_slice(&self.repaint_bytes());\r\n        out\r\n    }\r\n\r\n    /// Repaint row 1 WITHOUT disturbing the harness cursor: save cursor (DECSC),\r\n    /// home to row 1, clear the line, jump to the right-aligned start column, set\r\n    /// cyan, write the text, reset SGR, restore cursor (DECRC). The right-align\r\n    /// start column is `cols - width + 1` clamped to ≥ 1 (text wider than the\r\n    /// terminal starts at col 1 and is allowed to fill the row).\r\n    // [impl->REQ-RC-IDENTITY]\r\n    fn repaint_bytes(&self) -> Vec<u8> {\r\n        let width = self.display_width();\r\n        // Right-align: 1-based start column, clamped to col 1 when text ≥ cols.\r\n        let col = (self.cols as usize)\r\n            .saturating_sub(width)\r\n            .saturating_add(1)\r\n            .max(1);\r\n        let mut out = Vec::new();\r\n        out.extend_from_slice(b\"\\x1b7\"); // DECSC: save cursor\r\n        out.extend_from_slice(b\"\\x1b[1;1H\"); // home to row 1, col 1\r\n        out.extend_from_slice(b\"\\x1b[2K\"); // clear the whole line\r\n        out.extend_from_slice(format!(\"\\x1b[1;{col}H\").as_bytes()); // right-align start\r\n        out.extend_from_slice(b\"\\x1b[36m\"); // cyan\r\n        out.extend_from_slice(self.text.as_bytes());\r\n        out.extend_from_slice(b\"\\x1b[0m\"); // reset SGR\r\n        out.extend_from_slice(b\"\\x1b8\"); // DECRC: restore cursor\r\n        out\r\n    }\r\n\r\n    /// Tear the status down on detach: reset the scroll region to the full screen\r\n    /// (`ESC[r`), home to row 1, clear it — so the terminal is clean afterward.\r\n    // [impl->REQ-RC-IDENTITY]\r\n    fn restore_bytes() -> Vec<u8> {\r\n        let mut out = Vec::new();\r\n        out.extend_from_slice(b\"\\x1b[r\"); // reset scroll region (full screen)\r\n        out.extend_from_slice(b\"\\x1b[1;1H\"); // home to row 1\r\n        out.extend_from_slice(b\"\\x1b[2K\"); // clear the reclaimed row\r\n        out\r\n    }\r\n\r\n    /// Update the tracked terminal size on a window-change (the pump re-asserts\r\n    /// the margin + repaints after).\r\n    // [impl->REQ-RC-IDENTITY]\r\n    fn update_size(&mut self, rows: u16, cols: u16) {\r\n        self.rows = rows;\r\n        self.cols = cols;\r\n    }\r\n\r\n    /// The on-screen width of the banner. ASCII status text, so a `chars` count is\r\n    /// the cell width (no wide-char / combining concerns here).\r\n    fn display_width(&self) -> usize {\r\n        self.text.chars().count()\r\n    }\r\n}\r\n\r\n/// Resolve the controller's identity banner text (REQ-RC-IDENTITY):\r\n/// `{subnet} : {endpoint_id} @ {node}` — the endpoint's HOME subnet (`local` when\r\n/// unset), the attach target, and THIS machine's hostname label. An empty/absent\r\n/// node label omits the ` @ {node}` tail rather than rendering a lie. Resolved ONCE\r","truncated":true,"truncatedBy":"lines","totalLines":3242,"totalBytes":14299,"outputLines":303,"outputBytes":14299,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a\r\n//! local terminal to a broker-held PTY session.\r\n//!\r\n//! **One pump, swappable transport** (the doyle invariant). The attach protocol\r\n//! is `serve_attach` on the target side ⇄ this pump on the operator side, both\r\n//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the\r\n//! degenerate single-node case: the operator dials its OWN node (loopback peer)\r\n//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`\r\n//! against the same broker — the exact cross-node accept+serve path, no local\r\n//! special-case branch. The one-path invariant lives at this pump, not the\r\n//! transport under the stream.\r\n//!\r\n//! **Detach never kills the session.** The broker owns the PTY; closing our send\r\n//! side ends the viewport with the session still running (`serve_attach` →\r\n//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\r\n//! attach-lifecycle invariant).\r\n//!\r\n//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\r\n//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel\r\n//! (and watches for the detach keybind); the main loop interleaves draining that\r\n//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.\r\n// [impl->REQ-RC-1]\r\n\r\nuse std::io::{Read, Write};\r\nuse std::sync::atomic::{AtomicU64, Ordering};\r\nuse std::sync::mpsc;\r\nuse std::time::{Duration, Instant};\r\n\r\nuse spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\r\nuse spt_daemon::effect::{Minter, MintedOp};\r\nuse spt_daemon::brain::{now_ms, Brain, BrokerEvent};\r\nuse spt_daemon::msg::decode_bytes;\r\nuse spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};\r\n\r\n/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the\r\n/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one\r\n/// literal ctrl-b to the session; any other byte after the prefix is sent\r\n/// through verbatim (prefix + byte). Documented in the CLI help.\r\nconst DETACH_PREFIX: u8 = 0x02; // ctrl-b\r\nconst DETACH_KEY: u8 = b'd';\r\n\r\n/// How long the main loop blocks for an inbound event before looping back to\r\n/// drain stdin — the full-duplex interleave granularity.\r\nconst POLL_SLICE: Duration = Duration::from_millis(40);\r\n\r\n/// A message from the stdin reader thread to the pump.\r\nenum StdinMsg {\r\n    /// Raw bytes to forward to the session as `AttachRecord::Input`.\r\n    Bytes(Vec<u8>),\r\n    /// The operator hit the detach keybind — end the viewport, leave the session.\r\n    Detach,\r\n}\r\n\r\n/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\r\n/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for\r\n/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel\r\n/// events to us instead of scrolling its own buffer — so to keep scroll working we\r\n/// must FORWARD wheel events to the harness, but ONLY when the harness has asked\r\n/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these\r\n/// (scanning output); the Windows stdin reader READS them to decide whether to\r\n/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on\r\n/// all platforms (the scan is cheap + keeps the model correct); only the Windows\r\n/// reader acts on it (Unix terminals scroll natively, no capture).\r\n#[derive(Default)]\r\nstruct MouseMode {\r\n    /// The harness requested mouse reporting (DECSET 1000 / 1002 / 1003).\r\n    enabled: std::sync::atomic::AtomicBool,\r\n    /// The harness requested SGR extended coordinates (DECSET 1006). We forward a\r\n    /// scroll only when this is on (the modern encoding the harness will parse).\r\n    sgr: std::sync::atomic::AtomicBool,\r\n}\r\n\r\n/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset\r\n/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +\r\n/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT\r\n/// across two output chunks (the harness's mouse-enable can land across a decoder\r\n/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each\r\n/// `n` in {1000,1002,1003} sets `enabled = (final=='h')`, for 1006 sets `sgr`.\r\n#[derive(Default)]\r\nstruct MouseModeScanner {\r\n    /// A partial `ESC[?…` left dangling at a chunk boundary, prepended next feed.\r\n    carry: Vec<u8>,\r\n}\r\n\r\n/// The longest incomplete private-mode prefix we keep across a chunk boundary. A\r\n/// longer dangling `ESC[?…` with no final `h`/`l` is not a mode-set we care about\r\n/// (runaway) — dropped rather than buffered unboundedly.\r\nconst MOUSE_DECSET_CARRY_MAX: usize = 32;\r\n\r\nimpl MouseModeScanner {\r\n    /// Feed one output chunk; apply any complete DECSET mouse-mode toggles to\r\n    /// `mode`, carrying a trailing partial sequence to the next call.\r\n    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {\r\n        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.\r\n        if self.carry.is_empty() && !chunk.contains(&0x1b) {\r\n            return;\r\n        }\r\n        let mut buf = std::mem::take(&mut self.carry);\r\n        buf.extend_from_slice(chunk);\r\n        let mut i = 0;\r\n        while i < buf.len() {\r\n            if buf[i] != 0x1b {\r\n                i += 1;\r\n                continue;\r\n            }\r\n            match parse_decset_private(&buf[i..]) {\r\n                DecsetParse::Complete { consumed, params, set } => {\r\n                    for p in params {\r\n                        apply_mouse_mode(mode, p, set);\r\n                    }\r\n                    i += consumed;\r\n                }\r\n                // An incomplete `ESC[?…` at the tail: carry it (bounded) for the\r\n                // next chunk; a too-long dangling run is treated as not-a-DECSET.\r\n                DecsetParse::Incomplete => {\r\n                    let tail = &buf[i..];\r\n                    if tail.len() <= MOUSE_DECSET_CARRY_MAX {\r\n                        self.carry = tail.to_vec();\r\n                    }\r\n                    return;\r\n                }\r\n                // Not a private-mode sequence — skip this ESC, keep scanning.\r\n                DecsetParse::NotDecset => i += 1,\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Result of trying to parse a private-mode set/reset at the start of `s` (which\r\n/// begins with `ESC`).\r\nenum DecsetParse {\r\n    /// A complete `ESC[?<params>h|l`: the byte length consumed, the numeric\r\n    /// params, and `set` (`true` for `h`, `false` for `l`).\r\n    Complete { consumed: usize, params: Vec<u16>, set: bool },\r\n    /// `ESC[?…` with no final `h`/`l` yet (chunk boundary) — carry + retry.\r\n    Incomplete,\r\n    /// `s` does not start a private-mode sequence (`ESC` of something else).\r\n    NotDecset,\r\n}\r\n\r\n/// Parse a single `ESC[?<digits>[;<digits>…]h|l` private-mode sequence at the\r\n/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.\r\nfn parse_decset_private(s: &[u8]) -> DecsetParse {\r\n    // Need at least `ESC [ ?`.\r\n    if s.len() < 3 {\r\n        // Could be the very start of one split across the boundary.\r\n        if s.first() == Some(&0x1b) && s.get(1).is_none_or(|&b| b == b'[')\r\n            && s.get(2).is_none_or(|&b| b == b'?')\r\n        {\r\n            return DecsetParse::Incomplete;\r\n        }\r\n        return DecsetParse::NotDecset;\r\n    }\r\n    if s[1] != b'[' || s[2] != b'?' {\r\n        return DecsetParse::NotDecset;\r\n    }\r\n    let mut params: Vec<u16> = Vec::new();\r\n    let mut cur: Option<u32> = None;\r\n    let mut j = 3;\r\n    while j < s.len() {\r\n        match s[j] {\r\n            b'0'..=b'9' => {\r\n                let d = (s[j] - b'0') as u32;\r\n                cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));\r\n            }\r\n            b';' => {\r\n                params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);\r\n                cur = None;\r\n            }\r\n            b'h' | b'l' => {\r\n                if let Some(v) = cur {\r\n                    params.push(v.min(u16::MAX as u32) as u16);\r\n                }\r\n                return DecsetParse::Complete {\r\n                    consumed: j + 1,\r\n                    params,\r\n                    set: s[j] == b'h',\r\n                };\r\n            }\r\n            // Any other byte ⇒ this isn't a clean private-mode set/reset.\r\n            _ => return DecsetParse::NotDecset,\r\n        }\r\n        j += 1;\r\n    }\r\n    // Ran out before a final `h`/`l` ⇒ split across the chunk boundary.\r\n    DecsetParse::Incomplete\r\n}\r\n\r\n/// Apply one DECSET private-mode toggle to the tracked [`MouseMode`].\r\nfn apply_mouse_mode(mode: &MouseMode, param: u16, set: bool) {\r\n    use std::sync::atomic::Ordering::Release;\r\n    match param {\r\n        1000 | 1002 | 1003 => mode.enabled.store(set, Release),\r\n        1006 => mode.sgr.store(set, Release),\r\n        _ => {}\r\n    }\r\n}\r\n\r\n/// Bug #14 (REQ-RC-IDMARKER-DISABLE): ship the top-right endpoint-id status\r\n/// marker DISABLED. As a one-shot absolutely-positioned paint it scrolls off\r\n/// screen and is never re-stickied (#14), and its DECSC/clear/SGR injection\r\n/// splices into the harness's in-flight drawing, leaving residual artifacts on\r\n/// animate/scroll/resize (#7/#8). Operator ruling: OFF for now — revisit as a\r\n/// proper per-frame sticky overlay only once REQ-BROKER-SCREEN-GRID provides a\r\n/// server-side screen model (the concept is saved for a future web SPT GUI).\r\n/// Flip to `true` to restore the v0.16.0 W6 behavior (the StatusRow byte-emit\r\n/// machinery + its unit tests are kept intact, only the wiring is gated).\r\nconst STATUS_ROW_ENABLED: bool = false;\r\n\r\n/// Whether the controller reserved-row identity marker is active for this attach:\r\n/// only for a controller (a viewer's viewport is read-only) AND only while the\r\n/// marker is enabled (bug #14 ships it off).\r\n// [impl->REQ-RC-IDMARKER-DISABLE]\r\nfn status_row_active(view: bool) -> bool {\r\n    !view && STATUS_ROW_ENABLED\r\n}\r\n\r\n/// The CONTROLLER's reserved TOP status row (v0.16.0 W6, REQ-RC-IDENTITY): a\r\n/// one-line identity banner — `{subnet} : {endpoint_id} @ {node}` — painted on the\r\n/// real terminal's row 1, right-aligned + cyan, while the harness PTY runs in a\r\n/// scroll region reserving that row (DECSTBM). It tells the operator WHICH endpoint\r\n/// on WHICH node they are driving, so a multi-attach operator never types into the\r\n/// wrong session. Pure byte-emit (no I/O) so the exact VT shapes unit-test\r\n/// directly; the pump writes the bytes. Tracks the FULL terminal `rows` (it paints\r\n/// row 1 on the real terminal) while the PTY is told it has `rows - 1`.\r\n// [impl->REQ-RC-IDENTITY]\r\nstruct StatusRow {\r\n    /// The resolved identity line (ASCII status text, right-aligned on paint).\r\n    text: String,\r\n    /// The REAL terminal row count (the status owns row 1; the PTY gets rows-1).\r\n    rows: u16,\r\n    /// The terminal column count (the right-align anchor).\r\n    cols: u16,\r\n}\r\n\r\nimpl StatusRow {\r\n    /// Set the DECSTBM scroll region to reserve row 1, then paint the banner.\r\n    /// `ESC[2;{rows}r` makes the harness scroll within rows 2..=rows (the harness\r\n    /// never touches row 1), then [`repaint_bytes`](Self::repaint_bytes) draws it.\r\n    // [impl->REQ-RC-IDENTITY]\r\n    fn assert_bytes(&self) -> Vec<u8> {\r\n        let mut out = Vec::new();\r\n        // Reserve the top row: top margin = line 2, bottom margin = rows.\r\n        out.extend_from_slice(format!(\"\\x1b[2;{}r\", self.rows).as_bytes());\r\n        out.extend_from_slice(&self.repaint_bytes());\r\n        out\r\n    }\r\n\r\n    /// Repaint row 1 WITHOUT disturbing the harness cursor: save cursor (DECSC),\r\n    /// home to row 1, clear the line, jump to the right-aligned start column, set\r\n    /// cyan, write the text, reset SGR, restore cursor (DECRC). The right-align\r\n    /// start column is `cols - width + 1` clamped to ≥ 1 (text wider than the\r\n    /// terminal starts at col 1 and is allowed to fill the row).\r\n    // [impl->REQ-RC-IDENTITY]\r\n    fn repaint_bytes(&self) -> Vec<u8> {\r\n        let width = self.display_width();\r\n        // Right-align: 1-based start column, clamped to col 1 when text ≥ cols.\r\n        let col = (self.cols as usize)\r\n            .saturating_sub(width)\r\n            .saturating_add(1)\r\n            .max(1);\r\n        let mut out = Vec::new();\r\n        out.extend_from_slice(b\"\\x1b7\"); // DECSC: save cursor\r\n        out.extend_from_slice(b\"\\x1b[1;1H\"); // home to row 1, col 1\r\n        out.extend_from_slice(b\"\\x1b[2K\"); // clear the whole line\r\n        out.extend_from_slice(format!(\"\\x1b[1;{col}H\").as_bytes()); // right-align start\r\n        out.extend_from_slice(b\"\\x1b[36m\"); // cyan\r\n        out.extend_from_slice(self.text.as_bytes());\r\n        out.extend_from_slice(b\"\\x1b[0m\"); // reset SGR\r\n        out.extend_from_slice(b\"\\x1b8\"); // DECRC: restore cursor\r\n        out\r\n    }\r\n\r\n    /// Tear the status down on detach: reset the scroll region to the full screen\r\n    /// (`ESC[r`), home to row 1, clear it — so the terminal is clean afterward.\r\n    // [impl->REQ-RC-IDENTITY]\r\n    fn restore_bytes() -> Vec<u8> {\r\n        let mut out = Vec::new();\r\n        out.extend_from_slice(b\"\\x1b[r\"); // reset scroll region (full screen)\r\n        out.extend_from_slice(b\"\\x1b[1;1H\"); // home to row 1\r\n        out.extend_from_slice(b\"\\x1b[2K\"); // clear the reclaimed row\r\n        out\r\n    }\r\n\r\n    /// Update the tracked terminal size on a window-change (the pump re-asserts\r\n    /// the margin + repaints after).\r\n    // [impl->REQ-RC-IDENTITY]\r\n    fn update_size(&mut self, rows: u16, cols: u16) {\r\n        self.rows = rows;\r\n        self.cols = cols;\r\n    }\r\n\r\n    /// The on-screen width of the banner. ASCII status text, so a `chars` count is\r\n    /// the cell width (no wide-char / combining concerns here).\r\n    fn display_width(&self) -> usize {\r\n        self.text.chars().count()\r\n    }\r\n}\r\n\r\n/// Resolve the controller's identity banner text (REQ-RC-IDENTITY):\r\n/// `{subnet} : {endpoint_id} @ {node}` — the endpoint's HOME subnet (`local` when\r\n/// unset), the attach target, and THIS machine's hostname label. An empty/absent\r\n/// node label omits the ` @ {node}` tail rather than rendering a lie. Resolved ONCE\r","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":3242,"totalBytes":14299,"outputLines":303,"outputBytes":14299,"shownRange":{"start":1,"end":303},"nextOffset":304}}},"isError":false,"timestamp":1784365483383}}
{"type":"message","id":"7343164f","parentId":"ab91ceba","timestamp":"2026-07-18T09:04:43.430Z","message":{"role":"toolResult","toolCallId":"call_ZPrI5QgSaErXC24ZnnunegCL|fc_0ce5441d29b9f05d016a5b41ab2c0c819baf11db8bb72289f3","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…\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-263 of 889. Use :264 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…","truncated":true,"truncatedBy":"lines","totalLines":889,"totalBytes":64884,"outputLines":263,"outputBytes":64884,"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…\n\n…\n\n…\nThe CLI sibling of the frontend's guided resume — one command that lists endpoints **grouped by locality, most-recently-used within each group**: `on-node / current-project → on-node / other-project → off-node`, mirroring the *resolution policy*'s local-first preference. Selection **chains conditionally**: a **running** instance → attach/tap-in (no adapter step — already live under one); a **non-running** endpoint → into the **adapter selector** (*adapter selection*: history head = default → prior adapters → \"choose a different adapter\") → *home subnet* / other creation prompts as needed → launch; a **\"+ new endpoint\"** entry → the full creation flow. Off-node picks respect the reach + consent gates (remote-drive of your own running instance is ungated; a co…","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,null,294,null,464,null,888]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\CONTEXT.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":889,"totalBytes":64884,"outputLines":263,"outputBytes":64884,"shownRange":{"start":1,"end":263},"nextOffset":264},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784365483430}}
{"type":"message","id":"5b575a30","parentId":"7343164f","timestamp":"2026-07-18T09:04:43.438Z","message":{"role":"toolResult","toolCallId":"call_U0ZdOtiYAavmMntWKsAMkN2B|fc_0ce5441d29b9f05d016a5b41ab2c80819b9f0bfc4005cebfec","toolName":"grep","content":[{"type":"text","text":"# docs/\n## KNOWN-HAZARDS.md\n 133|<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\n*134|- **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 135|- **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 136|- **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 137|\n...\n 606|- **Failure (paid-for class):** a brain-only self-update restarts the BRAIN but keeps the BROKER process — and every one of its `Mutex`es — ALIVE by design (REQ-UPD-3). So a single panic while another thread held a broker-resident lock POISONS it permanently: the next `.lock().unwrap()` panics too, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read deadline elapsed\") while non-locked ops keep working. The effect journal (bug #16, `[REQ-HAZARD...\n*607|- **Invariant:** NO broker-resident lock reachable from serve/dispatch may use a bare `.lock().unwrap()`. Recover via the shared `recover` (`into_inner` — safe for the short, coherent-on-recovery map ops of `sessions`/`pair_holds`) or, for the `OutputLog` ring, `recover_log` — which adds a COHERENCE CLAMP on the recovery path: a panic mid-`append` can leave the ring torn (over-cap, a last seq not below `next_seq`, non-monotonic), and serving those bytes risks garbage, so `clamp_or_reset` cheap-checks...\n 608|- **spt-core mapping:** `broker::recover` / `broker::recover_log` / `OutputLog::clamp_or_reset`. unit = a poisoned session-map recovers to a usable guard (the next attach still opens), a torn ring clamps/resets-empty only when torn (coherent untouched, `next_seq` preserved), and `recover_log` wires the clamp onto the poison branch — each RED-first against a bare `.lock().unwrap()` / an un-clamped `into_inner`. int (scripted panic-under-lock during concurrent attach → next attach opens) deferred to th...\n 609|- **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 610|\n...\n 627|- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stal...\n*628|- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n 629|- **Source:** LIFECYCLE-TRUTH W2 (doyle rig `NtSuspendProcess` root + design ruling `docs/W2-DESIGN-RULING.md` @60ec0ed — minimal-plus scope, `BRAIN_WRITE_DEADLINE` 15 s, passive resubscribe + observability rider; todlando build 2026-07-07). The v0.27→v0.28 update-wedge night's flagship defect.\n 630|\n 631|### 7.37 The Layer-1 settle-gate must RE-ARM per delivery on an observable PTY — a mid-session reader reattach (`/clear`) re-creates the head-swallow race  `[REQ-HAZARD-INJECT-SETTLE-REARM]`\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 163|- Under the model-B trigger the outgoing brain is gone before the new one starts, so **no brain→brain message can be passed**; continuity must come from the persistent side. The broker already holds the per-session output ring (`broker.rs:73-143`); making it also track the delivered cursor lets a respawned brain resume gaplessly with **no disk writes** (respecting Q4) and **no handoff frame**.\n*164|- At-least-once output matches the *existing* terminal-stream contract: SPIKE-05 already established \"terminal-stream ≠ exactly-once transfer\" (a resize repaint reorders and duplicates the stream anyway). Exactly-once output would require a per-chunk ack protocol for a guarantee the terminal stream does not make; the thing that *must* be exactly-once (injected input/effects) already has the journal.\n 165|\n 166|**Alternative rejected.** *Add brain→broker acks so the broker resumes from last-processed (exactly-once output)* — cost without benefit, given the terminal-stream contract.\n 167|\n## MANIFEST.md\n 100|<!-- [doc->REQ-INSTALL-11] -->\n*101|**Install-dir program resolution (since v0.8.0).** A command template's bare program token (its first whitespace-split token, e.g. `claude-spt-digest …`) resolves against the **adapter's install dir** before PATH, so a `.spt` that ships its own binaries is self-contained — no PATH placement needed. spt-core probes `<install_dir>/<program>` (on Windows also the `.exe` suffix); when that file exists it runs that absolute path, otherwise it leaves the bare token for PATH to resolve. The install dir is t...\n 102|\n 103|```toml\n 104|# GO-LIVE GATE ONLY. Declaring [session.psyche_init] is the single signal that\n## NEXT-MILESTONE-BUG-TRIAGE.md\n 79|### D-#7 — PTY clutter as the TUI animates/scrolls  · STATUS: TRIAGE\n*80|Residual characters left in whitespace as the TUI animates (image: stray chars). Possibly related to the new top-right ID marker injection.\n 81|- Root cause: _TBD_\n 82|- Fix approach: _TBD_\n 83|\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*128|**D-#8 resize artifacts + marker vanishes** · conf H(marker)/M(artifacts) · effort S-disable/L-right · cross-platform. ROOT: marker is static absolutely-positioned one-shot, repainted only on detected size-change/reassert; resize reflow pushes row-1 banner into scrollback + re-emits dup cells; written-to-buffer not live-overlay → \"only visible if you scroll up\". NOT an axis-swap (size destructure correct). FIX: sticky overlay (needs #6 grid); until then disable.\n 129|\n 130|**D-#12 rc-to-headless garbled Win10/pwsh, fine Win11/WT** · conf H · effort S · **Win10-conhost-specific, INDEPENDENT**. ROOT: rc never enables VT OUTPUT on client stdout — `RawGuard::enable()` (rc.rs:729-741) only `enable_raw_mode()` (INPUT mode), NO `ENABLE_VIRTUAL_TERMINAL_PROCESSING` on STD_OUTPUT anywhere in spt. Win11 WT/new-conhost auto-enables VT-out; Win10 legacy conhost under raw pwsh leaves it off → ANSI prints literally (`←[K`). FIX: on Windows in RawGuard::enable, GetStdHandle(STD_...\n 131|\n*132|**D-#14 marker starts off-screen** · conf M-H · effort S-disable · coupled #7/#8 · cross-platform. ROOT: same StatusRow one-shot; cold attach paints row 1 but full-ring replay (#6)+scroll push it into scrollback, never re-stickies. REC: **ship DISABLED behind a feature flag now** — net-negative until #6 grid enables a proper per-frame sticky top-row repaint.\n 133|\n 134|**D sequencing:** (1) flag the ID-marker OFF now → cheaply fixes regression-half of #7/#8 + all #14. (2) #12 independent + cheap (VT-out enable) → do regardless. (3) #6 server-side grid = foundational, real fix for #6 + prereq for redoing marker as sticky overlay (#8/#14) + killing residual artifacts (#7). Key files: rc.rs (StatusRow/RawGuard/pump), broker.rs (ring+replay), attach.rs (from_seq=0), spt-term/{lib,stream,pty}.rs.\n 135|\n...\n 193|NEW: operator confirms `spt endpoint run --attach` renders cleanly in the SAME Win10/raw-pwsh environment; ONLY `spt rc` to a pre-running endpoint is garbled (raw color+cursor codes). This REFUTES \"VT-output never enabled\" (if so, --attach would garble too). Real question: what does the `rc`-to-existing path do DIFFERENTLY from `endpoint run --attach` in terminal/VT setup? Diff the two paths' client terminal-mode init.\n*194|- **Root cause (agent E, conf H): #12 IS A SURFACE OF #6 — fold them; prior VT-not-enabled theory REFUTED BY CONSTRUCTION.** Both paths call the SAME fn `run_attach_inner` (rc → session_confirmed=false rc.rs:1000; run --attach → =true rc.rs:993 via cli.rs:1702); the ONLY difference is the offline-failfast short-circuit (rc.rs:1047-1056). RawGuard::enable (rc.rs:730) calls only enable_raw_mode (NO ENABLE_VIRTUAL_TERMINAL_PROCESSING) in BOTH — so there's no client-VT difference to explain the asymm...\n 195|\n 196|---\n 197|\n...\n 205|\n*206|**#12 REFINED (operator):** \"ring rolled past setup\" is NOT guaranteed — a low-activity session could still hold its setup in the ring and render fine. The SOLID, dispositive fact stands: rc and `endpoint run --attach` are the SAME client code (VT theory refuted). The garbling is a server-side REPLAY-CONTENT problem (no grid model) — exact trigger varies (ring rolled past setup on a busy session, OR a torn sequence at the ring's oldest retained chunk, OR alt-screen/scroll-region state not reconstruct...\n 207|\n 208|**DESIGN-GATE RULINGS (operator 2026-06-30):**\n 209|- **#15 → DISPLAY-ONLY.** Leave resting.rs/CONTEXT.md as-is; fix the lone-instance \"Dormant\" label at the display layer (couples #11). No model change.\n## NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md\n 175|empty with a loud log — lost scrollback self-heals on the next PTY output\n*176|+ repaint; a permanent wedge does not. Sessions/pair_holds are short map\n 177|ops (coherent-on-recovery, same argument as the floor/journal). The\n 178|REQ-HAZARD-BROKER-FLOOR-LOCK-POISON seed is REDIRECTED to this class in\n 179|the same commit (redirect-folded-req-seeds discipline): floor half noted\n...\n 357|**E-2 — rc title replay (#2).** Root (certain): `GridState` has no\n*358|`osc_dispatch` (vte default drops OSC 0/2) and `render_repaint` emits no\n 359|title (screen.rs:687/571) — cold attach paints a perfect screen with a stale\n 360|title. Fix: `title: Option<String>` on the grid, capture OSC 0/2, clear on\n*361|RIS, emit `\\x1b]2;{title}\\x07` in repaint; flows through repaint_initial\n*362|automatically (broker.rs:463). ADR-0031 one-line amendment rider (repaint\n*363|also carries the last title). Red-first: feed title + text → repaint contains\n 364|both (screen.rs:898 pattern); attach.rs round-trip.\n 365|\n 366|**REQ seeds:** `REQ-SELF-DETECT-PARENT-PID` + dead-pid rider (E-1),\n## W2-GATE-VERDICT.md\n 70|covered by the pre-existing ring/cursor units — `apply_once…dedups_replay`,\n*71|cursor no-rewind CAS, clean-repaint attach — which W2's delta does not touch),\n 72|and note the live NtSuspendProcess run remains the manual field rig from the\n 73|incident night. The hazard registry is a conformance checklist; its evidence\n 74|lines must match delivered tests exactly.\n## W5-WAVE-GATE-REPORT.md\n 84|    BOTH `HEADSTART` and `TAILEND` per inject (payload sized to fit the 24-row grid so the head\n*85|    does not scroll off a cold-attach repaint). The ruling's binding \"loop N, byte-complete\".\n 86|  - `w5a_forced_echo_miss_redrives_once_then_loud_spools` — deterministic + cross-platform:\n 87|    `SPT_INJECT_FORCE_ECHO_MISS` forces the head-loss verdict → the binary is driven TWICE (one\n 88|    re-drive, asserted via the xlate stdin-log event count) and the envelope is re-spooled LOUD\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 73|2. ✅ **CLOSED (Spike #4, 2026-06-01).** Linux `forkpty` parity. Spike #1's binary, unchanged, passed all four invariants on `gravity-linux` (Ubuntu 22.04) — `portable-pty` selects `forkpty` on Unix; the ConPTY-DSR branch is inert there. Invariant B passed with the *strict* contiguity check that ConPTY-under-resize fails (Spike #5): `forkpty` is a raw pipe, ConPTY a screen buffer — different stream contracts, promoted into M3a's `SessionSurface` design. (`docs/spikes/SPIKE-04-forkpty-parity.md`.)\n*74|3. ✅ **CLOSED (Spike #5, 2026-06-01).** 100× restart + resize-under-load stress (codex #3). The broker survived 100 rapid brain kill/restart cycles + a resize thread racing the reader with no leak (one child, clean reap), no hang (watchdog deadline), no lost byte (full value-set coverage in the client's observation window). Surfaced two binding ConPTY findings for M3a/REQ-TERM-3: resize triggers a repaint that reorders+dups the stream (terminal-stream contract ≠ exactly-once transfer contract); mid-...\n 75|4. ✅ **CLOSED (Spike #6, 2026-06-01).** Idempotent/exactly-once delivery across brain restart (FATAL-adjacent #14). Durable-ID + WAL + dedup-at-effect proven exactly-once across a crash at any protocol point (before-intent / before-effect / after-effect). Two binding design constraints surfaced for `REQ-HAZARD-RESTART-IDEMPOTENT` / M3b-B5: broker-owned recovery anchor; dedup-at-effect keyed by durable ID. (`docs/spikes/SPIKE-06-idempotent-boundary.md`.)\n 76|\n 77|**Gate status (2026-06-01): ALL FOUR §E GAPS CLOSED PASS** (#3, #4, #5, #6) → the M3-PLAN Phase-0 spike-gate is **complete**. The broker/brain split is validated on both OSes (ConPTY + `forkpty`), across QUIC-stream survival, restart+resize churn, and exactly-once idempotency. M3a and M3b are both unblocked; the QUIC-ownership *implementation* and off-node transport remain explicitly deferred to M4 (only the *shape* was spiked).\n### 0008-live-activity-buffer-pty-digest.md\n 35|\n*36|**Why (the Gateway grill, 2026-06-12):** the PTY byte stream of an alternate-screen TUI is a **rendering protocol, not a content log**. Scroll and resize trigger full repaints — the same content re-enters the stream and corrupts delta computation — and semantic turn boundaries drown in repaint traffic. The original §Decision rationale (\"a light rolling regex over bytes ≠ fragile transcript parsing\") proved **backwards in practice**: the byte stream is the *more* fragile surface, and transcript par...\n 37|\n 38|**What stands (the product surface, unchanged):** the formula (N-turn window, collapsed tool sprints, presentation-vs-extraction split), structured source-tagged output, both access modes (snapshot pull + structured-delta stream, deltas now keyed to log records), address-gating, Option-C persistence. The no-built-in-parser rule **still holds** — normalization remains adapter-owned (Path A) or adapter-pushed (Path B).\n 39|\n### 0031-server-side-screen-grid-render-repaint.md\n*1|# Server-side screen grid + clean repaint on attach\n 2|\n 3|## Status\n 4|\n*5|accepted (2026-06-30) · companion to `V0.19.0-P6-SCREEN-GRID-DESIGN.md` · relates to [ADR-0008](0008-live-activity-buffer-pty-digest.md) (render-vs-content split) and [ADR-0001](0001-clean-room-fork-from-claude-skill-owl.md) (clean-room provenance).\n 6|\n 7|## Context\n 8|\n*9|A cold attach streams the entire retained **raw-byte ring** as its initial batch (`become_controller`, broker.rs:402-416). For an alt-screen TUI (Claude Code) that ring is a mid-stream rendering protocol; replayed raw into a fresh terminal it corrupts the screen + scrollback (field bugs #6/#7/#8). We need a server-side render model that interprets the byte stream into an authoritative current screen and synthesizes one clean repaint at attach.\n 10|\n 11|Two questions this ADR settles:\n 12|1. Does a server-side PTY-byte interpreter **contradict ADR-0008**, which retired the PTY-byte parser?\n...\n 16|\n*17|<!-- [doc->REQ-BROKER-SCREEN-GRID] the server-side render grid + clean repaint on attach:\n 18|     a clean-room `ScreenGrid` (vte::Perform) interprets the byte stream into an\n 19|     authoritative current screen; `become_controller`/`add_viewer` emit one synthesized\n*20|     repaint instead of the raw ring. Companion design: V0.19.0-P6-SCREEN-GRID-DESIGN.md. -->\n 21|\n*22|**1. The render grid does NOT reverse ADR-0008.** ADR-0008's governing split (2026-06-12 amendment) is: **render surfaces read the PTY; content surfaces read the logs.** It retired the PTY-byte parser only for **content** — the digest, which now projects from normalized session logs — because extracting *semantic turn content* from a repaint-heavy byte stream is fragile and harness-specific. The screen grid is a **render** surface: it reconstructs the *visible screen*, not conversation content. It is...\n 23|\n 24|**2. Take a vetted VT-parser dependency (`vte`), clean-room the grid.** The byte→action layer is `vte` (the alacritty terminal parser state machine): small, single-purpose, battle-tested against real-world escape sequences. The `ScreenGrid` (cells, cursor, main/alt buffer, modes) implementing `vte::Perform` is clean-room. Rationale under ADR-0001: ADR-0001 defaults to clean-room, but its intent is to avoid copying the sister project's evolving logic and to keep provenance clean — a **universal, stand...\n 25|\n*26|**Scope:** current screen only (viewport + cursor + alt/main). Scrollback stays the raw ring (history). The grid drives only the attach repaint. Wire format unchanged (`AttachRecord::Output`); only the initial-batch *content* changes. See the design doc for architecture, hazard integration, and the build plan.\n 27|\n*28|**Amendment (2026-07-06, REMOTE-TRUTH E-2):** the repaint also carries the child's **last-set window title** (OSC 0/2, captured on the grid, cleared by RIS) — a cold attach previously painted a perfect screen under the attacher's stale title because `vte`'s default drops OSC.\n 29|\n 30|## Consequences\n 31|\n 32|- New first-party dependency `vte`; folded into the release Cargo.lock first-party-name discipline (a version pin + audit at cut).\n*33|- The grid is the **OS-neutral render-replay layer** CONTEXT §terminal anticipated (forkpty raw-pipe vs ConPTY repaint-on-resize both feed one grid); it slots behind `SessionSurface`/`OutputStream`.\n*34|- `become_controller`/`add_viewer` emit a synthesized clean repaint as the initial batch; live frames after attach are unchanged raw bytes. Cross-node attach (#4) and any client are agnostic.\n 35|- Must preserve the attach/serve hazard invariants (REQ-HAZARD-INJECT-CONTROL-COEXIST 7.12, CONTROLLER-WRITER-REORDER 7.21, VIEWER-ISOLATION 7.7 + skip-to-live, the wedge family) — the grid is on the output path only, touches neither the input/effect journal nor the QUIC deadlines.\n 36|- Unlocks the follow-up sticky ID-marker redo (#8/#14 marker-half) as a grid-backed per-frame overlay (separate REQ).\n*37|- Composes with #12 (REQ-RC-WIN-VT-OUTPUT): the repaint is ANSI bytes; on Win10 conhost it renders only once VT-output-processing is enabled client-side (#12).\n\n## design/\n### v0.16.0-update-arc-and-cli.md\n 168|own the row), right-aligned **`SUBNET : ENDPOINT_ID @ NODE`**, **cyan** text. Re-assert the margin\n*169|+ repaint on alt-screen enter / `DECSTBM` reset / resize (scan output like the existing\n 170|`mouse_scanner`). **No window title** (CC owns it for busyness — OSC dropped). Resolve\n 171|subnet/node/id once at attach (perch/registry read), thread into the pump; subnet = the endpoint's\n 172|**home/primary** (`\"local\"` if none). The literal floating rounded-rect box is **deferred to the\n\n## spikes/\n### SPIKE-04-forkpty-parity.md\n 29|\n*30|Invariant **B passed with the STRICT monotonic-contiguity check** — the very check that *failed* on ConPTY-under-resize in Spike #5. `forkpty` does not repaint on resize and does not replay a viewport on attach; its master is a raw byte pipe, so the tick stream is strictly ordered with no duplication. This confirms the Spike #5 conclusion from the other side: **the two backends have different stream contracts**, and M3a's `SessionSurface` abstraction (REQ-TERM-1) must not assume ConPTY's repaint/viewpo...\n 31|\n 32|## Verdict\n 33|\n### SPIKE-05-restart-stress.md\n 32|\n*33|1. **Resize triggers a full repaint that re-emits buffered lines.** Under the resize thread, the client stream had ~4500 `tick` lines but only ~286 distinct values — a resize makes ConPTY redraw the current viewport, so the byte stream legitimately *duplicates and rewinds* (e.g. …301, 302, then back to 280…). **Strict monotonic contiguity (Spike #1's check) is wrong for a resizing ConPTY.** A terminal consumer is fine with this (it repaints identically); a *transfer* must never run over ConPTY.\n 34|\n 35|2. **A consumer attaching mid-session gets only the current viewport, not scrollback.** The \"missing\" values were exactly `0..16` — ticks emitted before the client connected (~1.4 s of startup). ConPTY replays only the visible viewport (~24 rows) on attach, not history. Within the client's actual observation window `[min_seen, max]`, coverage is complete — no churn loss.\n 36|\n*37|The correct \"no lost byte\" test is therefore **value-set coverage over the observation window** `[min_seen, max]`, robust to repaint reorder/dup and to viewport-only attach. Both findings are inputs to **REQ-TERM-3** (byte-stream streaming): M3a must define attach/replay semantics — does a remote terminal get scrollback or just the viewport? — and must NOT model the terminal stream as exactly-once-ordered bytes (that contract belongs to the transfer boundary, Spike #6).\n 38|\n 39|## What this does NOT prove\n 40|\n*41|- Linux `forkpty` resize/churn semantics (Spike #4 — `forkpty` is a raw pipe, no repaint, so its stream *is* monotonic; the two streams have different contracts).\n 42|- Long-duration leak detection beyond 100 cycles (no RSS/handle-count sampling here — A only asserts single-child + clean reap; a slow leak over thousands of cycles is out of scope).\n 43|\n 44|## Verdict\n\n# crates/spt-daemon/src/\n## brain.rs\n 320|    /// #6 (ADR-0031): a COLD attach (`from_seq == 0`, set by [`Brain::attach`] /\n*321|    /// [`Brain::attach_as`]) receives a synthesized clean-screen REPAINT as its\n 322|    /// first `KIND_OUTPUT` frame, at pseudo-seq `broker.next_seq - 1` — NOT seq 0.\n 323|    /// When `true`, the strict legacy `next_seq` path adopts that first frame's seq\n 324|    /// as the baseline (accept-and-seed) instead of reject-gapping it, then reverts\n...\n 727|                    // #6 (ADR-0031): a COLD attach's FIRST output frame is a\n*728|                    // clean-screen repaint at pseudo-seq `broker.next_seq - 1` (a\n 729|                    // forward jump from the requested `from_seq == 0`). Adopt that seq\n*730|                    // as the baseline instead of reject-gapping it — the repaint IS\n 731|                    // the complete current screen, nothing prior is owed. Consumed\n 732|                    // once per (cold) attach; every subsequent LIVE frame then takes\n 733|                    // the strict reject-gap below (a b4 drop still re-fetches — B2).\n...\n 961|        self.next_seq = from_seq;\n*962|        // #6 (ADR-0031): a COLD attach receives a clean-screen repaint at pseudo-seq\n 963|        // `broker.next_seq - 1`, not seq 0 — baseline the strict cursor on it.\n 964|        self.baseline_next_output = from_seq == 0;\n 965|        // A resume-mode brain (non-empty map) routes this session's output through\n...\n 992|        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n*993|        // --attach` case) gets a clean-screen repaint at pseudo-seq\n 994|        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n 995|        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n 996|        // this false → raw-ring replay + strict reject-gap unchanged (B2).\n## broker.rs\n 656|    delivered_through: Arc<AtomicU64>,\n*657|    /// The server-side **render grid** (#6 / REQ-BROKER-SCREEN-GRID, ADR-0031):\n 658|    /// the authoritative CURRENT SCREEN, interpreted from the same byte stream the\n 659|    /// ring records. Fed on the drain path in [`append`], AFTER the fan-out (so it\n 660|    /// never adds latency to controller/viewer delivery — hazard 7.12). A cold\n*661|    /// attach's initial batch is a single synthesized [`ScreenGrid::render_repaint`]\n 662|    /// instead of the raw ring, so replaying an alt-screen TUI into a fresh terminal\n 663|    /// no longer corrupts scrollback. Single-writer (the one drain thread), mirroring\n 664|    /// the ring's single producer.\n...\n 809|\n*810|    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\n 811|    /// carrying the current screen at pseudo-seq `next_seq - 1`.\n 812|    ///\n 813|    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\n...\n 815|    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n*816|    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n 817|    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\n 818|    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\n 819|    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\n 820|    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\n*821|    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\n 822|        if from_seq != 0 || self.next_seq == 0 {\n 823|            return None;\n 824|        }\n*825|        Some((self.next_seq - 1, self.grid.render_repaint()))\n 826|    }\n 827|\n 828|    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\n...\n 858|    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\n*859|    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\n 860|    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n 861|    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n 862|    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n...\n 964|        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n*965|        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n 966|        // snapshots it under the same short log lock only when an attach requests it.\n 967|        self.grid.advance(bytes);\n 968|        result\n...\n 1031|        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\n*1032|        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\n 1033|        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\n 1034|        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\n 1035|        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\n...\n 1037|        // it must not block the drain); live frames stream raw + in-order afterwards.\n*1038|        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\n*1039|            Some(repaint) => vec![repaint],\n 1040|            None => self\n 1041|                .ring\n 1042|                .iter()\n...\n 1225|        // The size frame + the initial batch (owned by the thread, not the bounded\n*1226|        // queue). A COLD viewer attach (`from_seq == 0`) gets the #6 clean repaint;\n 1227|        // a skip-to-live / snap re-attach (`from_seq > 0`, incl. `u64::MAX`) keeps\n 1228|        // the raw-ring slice (`s >= from_seq`) — empty for skip-to-live, as before.\n 1229|        let mut initial: Vec<Envelope> =\n 1230|            vec![size_envelope(self.session_id, self.size.0, self.size.1)];\n*1231|        match self.repaint_initial(from_seq) {\n 1232|            Some((seq, bytes)) => initial.push(output_envelope(self.session_id, seq, &bytes)),\n 1233|            None => initial.extend(\n 1234|                self.ring\n...\n 1359|        self.size = (rows, cols);\n*1360|        // Keep the render grid geometry in step so a post-resize repaint reflects\n 1361|        // the new viewport (#8 resize-artifact fix, ADR-0031).\n 1362|        self.grid.resize(rows, cols);\n 1363|        if self.viewers.is_empty() {\n...\n 2021|/// (+ loud log) on any violation. Lost scrollback self-heals on the next PTY output +\n*2022|/// repaint — strictly better than a permanent wedge (fail-fast reintroduces the very\n 2023|/// wedge B-1 kills) or a torn serve (blind recover). The healthy `Ok` path is a pure\n 2024|/// lock — no clamp, no cost on the hot append path.\n 2025|// [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n...\n 6082|    /// EPOCH-GATE KEYSTONE (REQ-HAZARD-CONTROLLER-WRITER-REORDER), post-#6. With\n*6083|    /// the ADR-0031 screen grid, a controller's initial batch is a SINGLE\n*6084|    /// synthesized clean repaint at pseudo-seq `next_seq - 1` (not the raw ring), so\n*6085|    /// the reorder hazard is now: a SUPERSEDED writer flushing its STALE repaint\n*6086|    /// ahead of the surviving writer's current one. We make the two repaints carry\n 6087|    /// DISTINCT seqs so a leak is order-detectable:\n 6088|    ///   - append seqs 0,1 → grid has content, `next_seq = 2`.\n*6089|    ///   - `become_controller(A, 0)` → writer-A initial = [repaint@1].\n 6090|    ///   - append seq 2 (grid advances) → `next_seq = 3`.\n## daemon.rs\n 1160|    let stdout = String::from_utf8_lossy(&out.stdout);\n*1161|    let mut nums = stdout.split_whitespace();\n 1162|    let return_value: i64 = nums\n 1163|        .next()\n 1164|        .and_then(|s| s.parse().ok())\n## digesthub.rs\n 419|\n*420|/// Collapse internal whitespace/newlines to a single spaced line for glanceable\n 421|/// rendering (the digest is a summary, not a transcript).\n 422|fn one_line(s: &str) -> String {\n*423|    s.split_whitespace().collect::<Vec<_>>().join(\" \")\n 424|}\n 425|\n 426|#[cfg(test)]\n## effect.rs\n 523|        let line = line?;\n*524|        let mut parts = line.split_whitespace();\n 525|        match parts.next() {\n 526|            Some(\"PENDING\") => {\n 527|                // OLD: `PENDING {class} {op} {kind}` (3 tokens after marker) → Legacy.\n## grants.rs\n 137|    /// (`consent-needed cap=… agent=… node=… [qual=…]`) + a human hint.\n*138|    /// Fields are SPT ids / pubkey hex / adapter names — whitespace-free by\n 139|    /// charset (REQ-HAZARD-ID-CHARSET), so space-separated `key=value` holds.\n 140|    // [impl->REQ-CONSENT-2]\n 141|    pub fn body(&self) -> String {\n...\n 167|        let line = body.lines().next()?;\n*168|        let mut parts = line.split_whitespace();\n 169|        if parts.next()? != ESCALATION_PREFIX {\n 170|            return None;\n 171|        }\n\n## bin/\n### xlate_choreo_fixture.rs\n 142|/// would false-pass). ~12 lines of ~70 cols ≈ 850 B — larger than the field's ~322 B\n*143|/// truncation and many settle-chunks wide, yet it fits inside a 24-row screen grid so a\n*144|/// cold-attach repaint (ADR-0031 = current screen) still carries the HEAD (a taller\n 145|/// payload scrolls HEADSTART off the visible grid).\n 146|fn big_multiline_payload() -> String {\n 147|    let mut s = String::from(\"HEADSTART \");\n\nShowing files 1-20 of 64+. Use skip=20 for the next page, or narrow paths/pattern.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"docs/KNOWN-HAZARDS.md, docs, crates","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":71,"fileCount":20,"files":["docs/KNOWN-HAZARDS.md","docs/BROKER-BRAIN-SPLIT-RESTORATION.md","docs/MANIFEST.md","docs/NEXT-MILESTONE-BUG-TRIAGE.md","docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md","docs/W2-GATE-VERDICT.md","docs/W5-WAVE-GATE-REPORT.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0008-live-activity-buffer-pty-digest.md","docs/adr/0031-server-side-screen-grid-render-repaint.md","docs/design/v0.16.0-update-arc-and-cli.md","docs/spikes/SPIKE-04-forkpty-parity.md","docs/spikes/SPIKE-05-restart-stress.md","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/grants.rs"],"fileMatches":[{"path":"docs/KNOWN-HAZARDS.md","count":3},{"path":"docs/BROKER-BRAIN-SPLIT-RESTORATION.md","count":1},{"path":"docs/MANIFEST.md","count":1},{"path":"docs/NEXT-MILESTONE-BUG-TRIAGE.md","count":8},{"path":"docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md","count":5},{"path":"docs/W2-GATE-VERDICT.md","count":1},{"path":"docs/W5-WAVE-GATE-REPORT.md","count":1},{"path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","count":1},{"path":"docs/adr/0008-live-activity-buffer-pty-digest.md","count":1},{"path":"docs/adr/0031-server-side-screen-grid-render-repaint.md","count":11},{"path":"docs/design/v0.16.0-update-arc-and-cli.md","count":1},{"path":"docs/spikes/SPIKE-04-forkpty-parity.md","count":1},{"path":"docs/spikes/SPIKE-05-restart-stress.md","count":3},{"path":"crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","count":2},{"path":"crates/spt-daemon/src/brain.rs","count":5},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/daemon.rs","count":1},{"path":"crates/spt-daemon/src/digesthub.rs","count":2},{"path":"crates/spt-daemon/src/effect.rs","count":1},{"path":"crates/spt-daemon/src/grants.rs","count":2}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# docs/\n## KNOWN-HAZARDS.md\n 133│<!-- [doc->REQ-HAZARD-ID-CHARSET] -->\n*134│- **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 135│- **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 136│- **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 137│\n    │...\n 606│- **Failure (paid-for class):** a brain-only self-update restarts the BRAIN but keeps the BROKER process — and every one of its `Mutex`es — ALIVE by design (REQ-UPD-3). So a single panic while another thread held a broker-resident lock POISONS it permanently: the next `.lock().unwrap()` panics too, kills its per-conn reply thread, and EVERY subsequent attach silently deadlines (`spt rc` → \"brain IPC read deadline elapsed\") while non-locked ops keep working. The effect journal (bug #16, `[REQ-HAZARD...\n*607│- **Invariant:** NO broker-resident lock reachable from serve/dispatch may use a bare `.lock().unwrap()`. Recover via the shared `recover` (`into_inner` — safe for the short, coherent-on-recovery map ops of `sessions`/`pair_holds`) or, for the `OutputLog` ring, `recover_log` — which adds a COHERENCE CLAMP on the recovery path: a panic mid-`append` can leave the ring torn (over-cap, a last seq not below `next_seq`, non-monotonic), and serving those bytes risks garbage, so `clamp_or_reset` cheap-checks...\n 608│- **spt-core mapping:** `broker::recover` / `broker::recover_log` / `OutputLog::clamp_or_reset`. unit = a poisoned session-map recovers to a usable guard (the next attach still opens), a torn ring clamps/resets-empty only when torn (coherent untouched, `next_seq` preserved), and `recover_log` wires the clamp onto the poison branch — each RED-first against a bare `.lock().unwrap()` / an un-clamped `into_inner`. int (scripted panic-under-lock during concurrent attach → next attach opens) deferred to th...\n 609│- **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 610│\n    │...\n 627│- **Invariant:** no broker client ever observes a wedge because the BRAIN stalled. (1) The controller's writer publishes its IN-FLIGHT-write window (`write_blocked_since`: `Some(Instant)` set immediately before the blocking `write_frame`, `None` after — the mutex never held ACROSS the write); a reader sees a `Some` older than `BRAIN_WRITE_DEADLINE` (15 s = 3× the 7.12 controller bound, measured full-with-**zero writer progress**, so a slow-but-DRAINING brain never trips it) as a WEDGED writer and stal...\n*628│- **spt-core mapping:** `broker.rs` — `ControllerSink::write_blocked_since` + `controller_writer` (marks the in-flight window around both the initial-batch and live-loop writes) + `controller_write_stalled` (pure predicate over injected `now`/`deadline`) + `stall_evict_controller` (Inline stamp at `resolve_subscribe`, Deferred stamp at `reap_dead_controller` so the reap closure does no I/O under the shared sessions lock) + `record_stall_evict`/`stall_evict_stats` (the tally) + `dispatch_stall_evicts` (...\n 629│- **Source:** LIFECYCLE-TRUTH W2 (doyle rig `NtSuspendProcess` root + design ruling `docs/W2-DESIGN-RULING.md` @60ec0ed — minimal-plus scope, `BRAIN_WRITE_DEADLINE` 15 s, passive resubscribe + observability rider; todlando build 2026-07-07). The v0.27→v0.28 update-wedge night's flagship defect.\n 630│\n 631│### 7.37 The Layer-1 settle-gate must RE-ARM per delivery on an observable PTY — a mid-session reader reattach (`/clear`) re-creates the head-swallow race  `[REQ-HAZARD-INJECT-SETTLE-REARM]`\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 163│- Under the model-B trigger the outgoing brain is gone before the new one starts, so **no brain→brain message can be passed**; continuity must come from the persistent side. The broker already holds the per-session output ring (`broker.rs:73-143`); making it also track the delivered cursor lets a respawned brain resume gaplessly with **no disk writes** (respecting Q4) and **no handoff frame**.\n*164│- At-least-once output matches the *existing* terminal-stream contract: SPIKE-05 already established \"terminal-stream ≠ exactly-once transfer\" (a resize repaint reorders and duplicates the stream anyway). Exactly-once output would require a per-chunk ack protocol for a guarantee the terminal stream does not make; the thing that *must* be exactly-once (injected input/effects) already has the journal.\n 165│\n 166│**Alternative rejected.** *Add brain→broker acks so the broker resumes from last-processed (exactly-once output)* — cost without benefit, given the terminal-stream contract.\n 167│\n## MANIFEST.md\n 100│<!-- [doc->REQ-INSTALL-11] -->\n*101│**Install-dir program resolution (since v0.8.0).** A command template's bare program token (its first whitespace-split token, e.g. `claude-spt-digest …`) resolves against the **adapter's install dir** before PATH, so a `.spt` that ships its own binaries is self-contained — no PATH placement needed. spt-core probes `<install_dir>/<program>` (on Windows also the `.exe` suffix); when that file exists it runs that absolute path, otherwise it leaves the bare token for PATH to resolve. The install dir is t...\n 102│\n 103│```toml\n 104│# GO-LIVE GATE ONLY. Declaring [session.psyche_init] is the single signal that\n## NEXT-MILESTONE-BUG-TRIAGE.md\n  79│### D-#7 — PTY clutter as the TUI animates/scrolls  · STATUS: TRIAGE\n *80│Residual characters left in whitespace as the TUI animates (image: stray chars). Possibly related to the new top-right ID marker injection.\n  81│- Root cause: _TBD_\n  82│- Fix approach: _TBD_\n  83│\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*128│**D-#8 resize artifacts + marker vanishes** · conf H(marker)/M(artifacts) · effort S-disable/L-right · cross-platform. ROOT: marker is static absolutely-positioned one-shot, repainted only on detected size-change/reassert; resize reflow pushes row-1 banner into scrollback + re-emits dup cells; written-to-buffer not live-overlay → \"only visible if you scroll up\". NOT an axis-swap (size destructure correct). FIX: sticky overlay (needs #6 grid); until then disable.\n 129│\n 130│**D-#12 rc-to-headless garbled Win10/pwsh, fine Win11/WT** · conf H · effort S · **Win10-conhost-specific, INDEPENDENT**. ROOT: rc never enables VT OUTPUT on client stdout — `RawGuard::enable()` (rc.rs:729-741) only `enable_raw_mode()` (INPUT mode), NO `ENABLE_VIRTUAL_TERMINAL_PROCESSING` on STD_OUTPUT anywhere in spt. Win11 WT/new-conhost auto-enables VT-out; Win10 legacy conhost under raw pwsh leaves it off → ANSI prints literally (`←[K`). FIX: on Windows in RawGuard::enable, GetStdHandle(STD_...\n 131│\n*132│**D-#14 marker starts off-screen** · conf M-H · effort S-disable · coupled #7/#8 · cross-platform. ROOT: same StatusRow one-shot; cold attach paints row 1 but full-ring replay (#6)+scroll push it into scrollback, never re-stickies. REC: **ship DISABLED behind a feature flag now** — net-negative until #6 grid enables a proper per-frame sticky top-row repaint.\n 133│\n 134│**D sequencing:** (1) flag the ID-marker OFF now → cheaply fixes regression-half of #7/#8 + all #14. (2) #12 independent + cheap (VT-out enable) → do regardless. (3) #6 server-side grid = foundational, real fix for #6 + prereq for redoing marker as sticky overlay (#8/#14) + killing residual artifacts (#7). Key files: rc.rs (StatusRow/RawGuard/pump), broker.rs (ring+replay), attach.rs (from_seq=0), spt-term/{lib,stream,pty}.rs.\n 135│\n    │...\n 193│NEW: operator confirms `spt endpoint run --attach` renders cleanly in the SAME Win10/raw-pwsh environment; ONLY `spt rc` to a pre-running endpoint is garbled (raw color+cursor codes). This REFUTES \"VT-output never enabled\" (if so, --attach would garble too). Real question: what does the `rc`-to-existing path do DIFFERENTLY from `endpoint run --attach` in terminal/VT setup? Diff the two paths' client terminal-mode init.\n*194│- **Root cause (agent E, conf H): #12 IS A SURFACE OF #6 — fold them; prior VT-not-enabled theory REFUTED BY CONSTRUCTION.** Both paths call the SAME fn `run_attach_inner` (rc → session_confirmed=false rc.rs:1000; run --attach → =true rc.rs:993 via cli.rs:1702); the ONLY difference is the offline-failfast short-circuit (rc.rs:1047-1056). RawGuard::enable (rc.rs:730) calls only enable_raw_mode (NO ENABLE_VIRTUAL_TERMINAL_PROCESSING) in BOTH — so there's no client-VT difference to explain the asymm...\n 195│\n 196│---\n 197│\n    │...\n 205│\n*206│**#12 REFINED (operator):** \"ring rolled past setup\" is NOT guaranteed — a low-activity session could still hold its setup in the ring and render fine. The SOLID, dispositive fact stands: rc and `endpoint run --attach` are the SAME client code (VT theory refuted). The garbling is a server-side REPLAY-CONTENT problem (no grid model) — exact trigger varies (ring rolled past setup on a busy session, OR a torn sequence at the ring's oldest retained chunk, OR alt-screen/scroll-region state not reconstruct...\n 207│\n 208│**DESIGN-GATE RULINGS (operator 2026-06-30):**\n 209│- **#15 → DISPLAY-ONLY.** Leave resting.rs/CONTEXT.md as-is; fix the lone-instance \"Dormant\" label at the display layer (couples #11). No model change.\n## NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md\n 175│empty with a loud log — lost scrollback self-heals on the next PTY output\n*176│+ repaint; a permanent wedge does not. Sessions/pair_holds are short map\n 177│ops (coherent-on-recovery, same argument as the floor/journal). The\n 178│REQ-HAZARD-BROKER-FLOOR-LOCK-POISON seed is REDIRECTED to this class in\n 179│the same commit (redirect-folded-req-seeds discipline): floor half noted\n    │...\n 357│**E-2 — rc title replay (#2).** Root (certain): `GridState` has no\n*358│`osc_dispatch` (vte default drops OSC 0/2) and `render_repaint` emits no\n 359│title (screen.rs:687/571) — cold attach paints a perfect screen with a stale\n 360│title. Fix: `title: Option<String>` on the grid, capture OSC 0/2, clear on\n*361│RIS, emit `\\x1b]2;{title}\\x07` in repaint; flows through repaint_initial\n*362│automatically (broker.rs:463). ADR-0031 one-line amendment rider (repaint\n*363│also carries the last title). Red-first: feed title + text → repaint contains\n 364│both (screen.rs:898 pattern); attach.rs round-trip.\n 365│\n 366│**REQ seeds:** `REQ-SELF-DETECT-PARENT-PID` + dead-pid rider (E-1),\n## W2-GATE-VERDICT.md\n 70│covered by the pre-existing ring/cursor units — `apply_once…dedups_replay`,\n*71│cursor no-rewind CAS, clean-repaint attach — which W2's delta does not touch),\n 72│and note the live NtSuspendProcess run remains the manual field rig from the\n 73│incident night. The hazard registry is a conformance checklist; its evidence\n 74│lines must match delivered tests exactly.\n## W5-WAVE-GATE-REPORT.md\n 84│    BOTH `HEADSTART` and `TAILEND` per inject (payload sized to fit the 24-row grid so the head\n*85│    does not scroll off a cold-attach repaint). The ruling's binding \"loop N, byte-complete\".\n 86│  - `w5a_forced_echo_miss_redrives_once_then_loud_spools` — deterministic + cross-platform:\n 87│    `SPT_INJECT_FORCE_ECHO_MISS` forces the head-loss verdict → the binary is driven TWICE (one\n 88│    re-drive, asserted via the xlate stdin-log event count) and the envelope is re-spooled LOUD\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 73│2. ✅ **CLOSED (Spike #4, 2026-06-01).** Linux `forkpty` parity. Spike #1's binary, unchanged, passed all four invariants on `gravity-linux` (Ubuntu 22.04) — `portable-pty` selects `forkpty` on Unix; the ConPTY-DSR branch is inert there. Invariant B passed with the *strict* contiguity check that ConPTY-under-resize fails (Spike #5): `forkpty` is a raw pipe, ConPTY a screen buffer — different stream contracts, promoted into M3a's `SessionSurface` design. (`docs/spikes/SPIKE-04-forkpty-parity.md`.)\n*74│3. ✅ **CLOSED (Spike #5, 2026-06-01).** 100× restart + resize-under-load stress (codex #3). The broker survived 100 rapid brain kill/restart cycles + a resize thread racing the reader with no leak (one child, clean reap), no hang (watchdog deadline), no lost byte (full value-set coverage in the client's observation window). Surfaced two binding ConPTY findings for M3a/REQ-TERM-3: resize triggers a repaint that reorders+dups the stream (terminal-stream contract ≠ exactly-once transfer contract); mid-...\n 75│4. ✅ **CLOSED (Spike #6, 2026-06-01).** Idempotent/exactly-once delivery across brain restart (FATAL-adjacent #14). Durable-ID + WAL + dedup-at-effect proven exactly-once across a crash at any protocol point (before-intent / before-effect / after-effect). Two binding design constraints surfaced for `REQ-HAZARD-RESTART-IDEMPOTENT` / M3b-B5: broker-owned recovery anchor; dedup-at-effect keyed by durable ID. (`docs/spikes/SPIKE-06-idempotent-boundary.md`.)\n 76│\n 77│**Gate status (2026-06-01): ALL FOUR §E GAPS CLOSED PASS** (#3, #4, #5, #6) → the M3-PLAN Phase-0 spike-gate is **complete**. The broker/brain split is validated on both OSes (ConPTY + `forkpty`), across QUIC-stream survival, restart+resize churn, and exactly-once idempotency. M3a and M3b are both unblocked; the QUIC-ownership *implementation* and off-node transport remain explicitly deferred to M4 (only the *shape* was spiked).\n### 0008-live-activity-buffer-pty-digest.md\n 35│\n*36│**Why (the Gateway grill, 2026-06-12):** the PTY byte stream of an alternate-screen TUI is a **rendering protocol, not a content log**. Scroll and resize trigger full repaints — the same content re-enters the stream and corrupts delta computation — and semantic turn boundaries drown in repaint traffic. The original §Decision rationale (\"a light rolling regex over bytes ≠ fragile transcript parsing\") proved **backwards in practice**: the byte stream is the *more* fragile surface, and transcript par...\n 37│\n 38│**What stands (the product surface, unchanged):** the formula (N-turn window, collapsed tool sprints, presentation-vs-extraction split), structured source-tagged output, both access modes (snapshot pull + structured-delta stream, deltas now keyed to log records), address-gating, Option-C persistence. The no-built-in-parser rule **still holds** — normalization remains adapter-owned (Path A) or adapter-pushed (Path B).\n 39│\n### 0031-server-side-screen-grid-render-repaint.md\n *1│# Server-side screen grid + clean repaint on attach\n  2│\n  3│## Status\n  4│\n *5│accepted (2026-06-30) · companion to `V0.19.0-P6-SCREEN-GRID-DESIGN.md` · relates to [ADR-0008](0008-live-activity-buffer-pty-digest.md) (render-vs-content split) and [ADR-0001](0001-clean-room-fork-from-claude-skill-owl.md) (clean-room provenance).\n  6│\n  7│## Context\n  8│\n *9│A cold attach streams the entire retained **raw-byte ring** as its initial batch (`become_controller`, broker.rs:402-416). For an alt-screen TUI (Claude Code) that ring is a mid-stream rendering protocol; replayed raw into a fresh terminal it corrupts the screen + scrollback (field bugs #6/#7/#8). We need a server-side render model that interprets the byte stream into an authoritative current screen and synthesizes one clean repaint at attach.\n 10│\n 11│Two questions this ADR settles:\n 12│1. Does a server-side PTY-byte interpreter **contradict ADR-0008**, which retired the PTY-byte parser?\n   │...\n 16│\n*17│<!-- [doc->REQ-BROKER-SCREEN-GRID] the server-side render grid + clean repaint on attach:\n 18│     a clean-room `ScreenGrid` (vte::Perform) interprets the byte stream into an\n 19│     authoritative current screen; `become_controller`/`add_viewer` emit one synthesized\n*20│     repaint instead of the raw ring. Companion design: V0.19.0-P6-SCREEN-GRID-DESIGN.md. -->\n 21│\n*22│**1. The render grid does NOT reverse ADR-0008.** ADR-0008's governing split (2026-06-12 amendment) is: **render surfaces read the PTY; content surfaces read the logs.** It retired the PTY-byte parser only for **content** — the digest, which now projects from normalized session logs — because extracting *semantic turn content* from a repaint-heavy byte stream is fragile and harness-specific. The screen grid is a **render** surface: it reconstructs the *visible screen*, not conversation content. It is...\n 23│\n 24│**2. Take a vetted VT-parser dependency (`vte`), clean-room the grid.** The byte→action layer is `vte` (the alacritty terminal parser state machine): small, single-purpose, battle-tested against real-world escape sequences. The `ScreenGrid` (cells, cursor, main/alt buffer, modes) implementing `vte::Perform` is clean-room. Rationale under ADR-0001: ADR-0001 defaults to clean-room, but its intent is to avoid copying the sister project's evolving logic and to keep provenance clean — a **universal, stand...\n 25│\n*26│**Scope:** current screen only (viewport + cursor + alt/main). Scrollback stays the raw ring (history). The grid drives only the attach repaint. Wire format unchanged (`AttachRecord::Output`); only the initial-batch *content* changes. See the design doc for architecture, hazard integration, and the build plan.\n 27│\n*28│**Amendment (2026-07-06, REMOTE-TRUTH E-2):** the repaint also carries the child's **last-set window title** (OSC 0/2, captured on the grid, cleared by RIS) — a cold attach previously painted a perfect screen under the attacher's stale title because `vte`'s default drops OSC.\n 29│\n 30│## Consequences\n 31│\n 32│- New first-party dependency `vte`; folded into the release Cargo.lock first-party-name discipline (a version pin + audit at cut).\n*33│- The grid is the **OS-neutral render-replay layer** CONTEXT §terminal anticipated (forkpty raw-pipe vs ConPTY repaint-on-resize both feed one grid); it slots behind `SessionSurface`/`OutputStream`.\n*34│- `become_controller`/`add_viewer` emit a synthesized clean repaint as the initial batch; live frames after attach are unchanged raw bytes. Cross-node attach (#4) and any client are agnostic.\n 35│- Must preserve the attach/serve hazard invariants (REQ-HAZARD-INJECT-CONTROL-COEXIST 7.12, CONTROLLER-WRITER-REORDER 7.21, VIEWER-ISOLATION 7.7 + skip-to-live, the wedge family) — the grid is on the output path only, touches neither the input/effect journal nor the QUIC deadlines.\n 36│- Unlocks the follow-up sticky ID-marker redo (#8/#14 marker-half) as a grid-backed per-frame overlay (separate REQ).\n*37│- Composes with #12 (REQ-RC-WIN-VT-OUTPUT): the repaint is ANSI bytes; on Win10 conhost it renders only once VT-output-processing is enabled client-side (#12).\n\n## design/\n### v0.16.0-update-arc-and-cli.md\n 168│own the row), right-aligned **`SUBNET : ENDPOINT_ID @ NODE`**, **cyan** text. Re-assert the margin\n*169│+ repaint on alt-screen enter / `DECSTBM` reset / resize (scan output like the existing\n 170│`mouse_scanner`). **No window title** (CC owns it for busyness — OSC dropped). Resolve\n 171│subnet/node/id once at attach (perch/registry read), thread into the pump; subnet = the endpoint's\n 172│**home/primary** (`\"local\"` if none). The literal floating rounded-rect box is **deferred to the\n\n## spikes/\n### SPIKE-04-forkpty-parity.md\n 29│\n*30│Invariant **B passed with the STRICT monotonic-contiguity check** — the very check that *failed* on ConPTY-under-resize in Spike #5. `forkpty` does not repaint on resize and does not replay a viewport on attach; its master is a raw byte pipe, so the tick stream is strictly ordered with no duplication. This confirms the Spike #5 conclusion from the other side: **the two backends have different stream contracts**, and M3a's `SessionSurface` abstraction (REQ-TERM-1) must not assume ConPTY's repaint/viewpo...\n 31│\n 32│## Verdict\n 33│\n### SPIKE-05-restart-stress.md\n 32│\n*33│1. **Resize triggers a full repaint that re-emits buffered lines.** Under the resize thread, the client stream had ~4500 `tick` lines but only ~286 distinct values — a resize makes ConPTY redraw the current viewport, so the byte stream legitimately *duplicates and rewinds* (e.g. …301, 302, then back to 280…). **Strict monotonic contiguity (Spike #1's check) is wrong for a resizing ConPTY.** A terminal consumer is fine with this (it repaints identically); a *transfer* must never run over ConPTY.\n 34│\n 35│2. **A consumer attaching mid-session gets only the current viewport, not scrollback.** The \"missing\" values were exactly `0..16` — ticks emitted before the client connected (~1.4 s of startup). ConPTY replays only the visible viewport (~24 rows) on attach, not history. Within the client's actual observation window `[min_seen, max]`, coverage is complete — no churn loss.\n 36│\n*37│The correct \"no lost byte\" test is therefore **value-set coverage over the observation window** `[min_seen, max]`, robust to repaint reorder/dup and to viewport-only attach. Both findings are inputs to **REQ-TERM-3** (byte-stream streaming): M3a must define attach/replay semantics — does a remote terminal get scrollback or just the viewport? — and must NOT model the terminal stream as exactly-once-ordered bytes (that contract belongs to the transfer boundary, Spike #6).\n 38│\n 39│## What this does NOT prove\n 40│\n*41│- Linux `forkpty` resize/churn semantics (Spike #4 — `forkpty` is a raw pipe, no repaint, so its stream *is* monotonic; the two streams have different contracts).\n 42│- Long-duration leak detection beyond 100 cycles (no RSS/handle-count sampling here — A only asserts single-child + clean reap; a slow leak over thousands of cycles is out of scope).\n 43│\n 44│## Verdict\n\n# crates/spt-daemon/src/\n## brain.rs\n 320│    /// #6 (ADR-0031): a COLD attach (`from_seq == 0`, set by [`Brain::attach`] /\n*321│    /// [`Brain::attach_as`]) receives a synthesized clean-screen REPAINT as its\n 322│    /// first `KIND_OUTPUT` frame, at pseudo-seq `broker.next_seq - 1` — NOT seq 0.\n 323│    /// When `true`, the strict legacy `next_seq` path adopts that first frame's seq\n 324│    /// as the baseline (accept-and-seed) instead of reject-gapping it, then reverts\n    │...\n 727│                    // #6 (ADR-0031): a COLD attach's FIRST output frame is a\n*728│                    // clean-screen repaint at pseudo-seq `broker.next_seq - 1` (a\n 729│                    // forward jump from the requested `from_seq == 0`). Adopt that seq\n*730│                    // as the baseline instead of reject-gapping it — the repaint IS\n 731│                    // the complete current screen, nothing prior is owed. Consumed\n 732│                    // once per (cold) attach; every subsequent LIVE frame then takes\n 733│                    // the strict reject-gap below (a b4 drop still re-fetches — B2).\n    │...\n 961│        self.next_seq = from_seq;\n*962│        // #6 (ADR-0031): a COLD attach receives a clean-screen repaint at pseudo-seq\n 963│        // `broker.next_seq - 1`, not seq 0 — baseline the strict cursor on it.\n 964│        self.baseline_next_output = from_seq == 0;\n 965│        // A resume-mode brain (non-empty map) routes this session's output through\n    │...\n 992│        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n*993│        // --attach` case) gets a clean-screen repaint at pseudo-seq\n 994│        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n 995│        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n 996│        // this false → raw-ring replay + strict reject-gap unchanged (B2).\n## broker.rs\n  656│    delivered_through: Arc<AtomicU64>,\n *657│    /// The server-side **render grid** (#6 / REQ-BROKER-SCREEN-GRID, ADR-0031):\n  658│    /// the authoritative CURRENT SCREEN, interpreted from the same byte stream the\n  659│    /// ring records. Fed on the drain path in [`append`], AFTER the fan-out (so it\n  660│    /// never adds latency to controller/viewer delivery — hazard 7.12). A cold\n *661│    /// attach's initial batch is a single synthesized [`ScreenGrid::render_repaint`]\n  662│    /// instead of the raw ring, so replaying an alt-screen TUI into a fresh terminal\n  663│    /// no longer corrupts scrollback. Single-writer (the one drain thread), mirroring\n  664│    /// the ring's single producer.\n     │...\n  809│\n *810│    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\n  811│    /// carrying the current screen at pseudo-seq `next_seq - 1`.\n  812│    ///\n  813│    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\n     │...\n  815│    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n *816│    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n  817│    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\n  818│    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\n  819│    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\n  820│    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\n *821│    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\n  822│        if from_seq != 0 || self.next_seq == 0 {\n  823│            return None;\n  824│        }\n *825│        Some((self.next_seq - 1, self.grid.render_repaint()))\n  826│    }\n  827│\n  828│    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\n     │...\n  858│    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\n *859│    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\n  860│    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n  861│    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n  862│    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n     │...\n  964│        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n *965│        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n  966│        // snapshots it under the same short log lock only when an attach requests it.\n  967│        self.grid.advance(bytes);\n  968│        result\n     │...\n 1031│        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\n*1032│        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\n 1033│        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\n 1034│        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\n 1035│        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\n     │...\n 1037│        // it must not block the drain); live frames stream raw + in-order afterwards.\n*1038│        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\n*1039│            Some(repaint) => vec![repaint],\n 1040│            None => self\n 1041│                .ring\n 1042│                .iter()\n     │...\n 1225│        // The size frame + the initial batch (owned by the thread, not the bounded\n*1226│        // queue). A COLD viewer attach (`from_seq == 0`) gets the #6 clean repaint;\n 1227│        // a skip-to-live / snap re-attach (`from_seq > 0`, incl. `u64::MAX`) keeps\n 1228│        // the raw-ring slice (`s >= from_seq`) — empty for skip-to-live, as before.\n 1229│        let mut initial: Vec<Envelope> =\n 1230│            vec![size_envelope(self.session_id, self.size.0, self.size.1)];\n*1231│        match self.repaint_initial(from_seq) {\n 1232│            Some((seq, bytes)) => initial.push(output_envelope(self.session_id, seq, &bytes)),\n 1233│            None => initial.extend(\n 1234│                self.ring\n     │...\n 1359│        self.size = (rows, cols);\n*1360│        // Keep the render grid geometry in step so a post-resize repaint reflects\n 1361│        // the new viewport (#8 resize-artifact fix, ADR-0031).\n 1362│        self.grid.resize(rows, cols);\n 1363│        if self.viewers.is_empty() {\n     │...\n 2021│/// (+ loud log) on any violation. Lost scrollback self-heals on the next PTY output +\n*2022│/// repaint — strictly better than a permanent wedge (fail-fast reintroduces the very\n 2023│/// wedge B-1 kills) or a torn serve (blind recover). The healthy `Ok` path is a pure\n 2024│/// lock — no clamp, no cost on the hot append path.\n 2025│// [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n     │...\n 6082│    /// EPOCH-GATE KEYSTONE (REQ-HAZARD-CONTROLLER-WRITER-REORDER), post-#6. With\n*6083│    /// the ADR-0031 screen grid, a controller's initial batch is a SINGLE\n*6084│    /// synthesized clean repaint at pseudo-seq `next_seq - 1` (not the raw ring), so\n*6085│    /// the reorder hazard is now: a SUPERSEDED writer flushing its STALE repaint\n*6086│    /// ahead of the surviving writer's current one. We make the two repaints carry\n 6087│    /// DISTINCT seqs so a leak is order-detectable:\n 6088│    ///   - append seqs 0,1 → grid has content, `next_seq = 2`.\n*6089│    ///   - `become_controller(A, 0)` → writer-A initial = [repaint@1].\n 6090│    ///   - append seq 2 (grid advances) → `next_seq = 3`.\n## daemon.rs\n 1160│    let stdout = String::from_utf8_lossy(&out.stdout);\n*1161│    let mut nums = stdout.split_whitespace();\n 1162│    let return_value: i64 = nums\n 1163│        .next()\n 1164│        .and_then(|s| s.parse().ok())\n## digesthub.rs\n 419│\n*420│/// Collapse internal whitespace/newlines to a single spaced line for glanceable\n 421│/// rendering (the digest is a summary, not a transcript).\n 422│fn one_line(s: &str) -> String {\n*423│    s.split_whitespace().collect::<Vec<_>>().join(\" \")\n 424│}\n 425│\n 426│#[cfg(test)]\n## effect.rs\n 523│        let line = line?;\n*524│        let mut parts = line.split_whitespace();\n 525│        match parts.next() {\n 526│            Some(\"PENDING\") => {\n 527│                // OLD: `PENDING {class} {op} {kind}` (3 tokens after marker) → Legacy.\n## grants.rs\n 137│    /// (`consent-needed cap=… agent=… node=… [qual=…]`) + a human hint.\n*138│    /// Fields are SPT ids / pubkey hex / adapter names — whitespace-free by\n 139│    /// charset (REQ-HAZARD-ID-CHARSET), so space-separated `key=value` holds.\n 140│    // [impl->REQ-CONSENT-2]\n 141│    pub fn body(&self) -> String {\n    │...\n 167│        let line = body.lines().next()?;\n*168│        let mut parts = line.split_whitespace();\n 169│        if parts.next()? != ESCALATION_PREFIX {\n 170│            return None;\n 171│        }\n\n## bin/\n### xlate_choreo_fixture.rs\n 142│/// would false-pass). ~12 lines of ~70 cols ≈ 850 B — larger than the field's ~322 B\n*143│/// truncation and many settle-chunks wide, yet it fits inside a 24-row screen grid so a\n*144│/// cold-attach repaint (ADR-0031 = current screen) still carries the HEAD (a taller\n 145│/// payload scrolls HEADSTART off the visible grid).\n 146│fn big_multiline_payload() -> String {\n 147│    let mut s = String::from(\"HEADSTART \");","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784365483438}}
{"type":"message","id":"0933cec1","parentId":"5b575a30","timestamp":"2026-07-18T09:04:43.483Z","message":{"role":"toolResult","toolCallId":"call_y3MKcLPOpl9Vv0HqFkd83jvZ|fc_0ce5441d29b9f05d016a5b41ab2c9c819b84fd6572838da68a","toolName":"read","content":[{"type":"text","text":"struct OutputLog {\n…\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/// (`driven_by`, `controlled`, `viewer_count`) differ from the live session TRUTH?\n/// Returns `(control_write_needed, viewer_write_needed)` — the divergence gate that\n/// keeps the per-poll re-assert from writing (fsync) when nothing changed.\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\nfn stamp_divergence(\n    rec_driven_by: Option<&str>,\n    rec_controlled: bool,\n    rec_viewers: u32,\n    want_driven_by: Option<&str>,\n    want_controlled: bool,\n    want_viewers: u32,\n) -> (bool, bool) {\n    let control = rec_driven_by != want_driven_by || rec_controlled != want_controlled;\n    let viewer = rec_viewers != want_viewers;\n    (control, viewer)\n}\n\n/// Per-endpoint stamp-transition serial (ADR-0041 decision 4,\n/// REQ-CONTROL-STAMP-CONVERGENCE): the generation counter a definitive\n/// no-session truth (the exit-waiter reap) BUMPS, and a poll-derived stamp\n/// write VALIDATES against — so a `KIND_SESSIONS` snapshot taken PRE-reap can\n/// never relatch `controlled=true` after the reap cleared it (the session is\n/// gone; no later poll would ever heal the latch). The `write` mutex\n/// serializes the validate-then-write pair against the bump-then-clear pair —\n/// without it the poll could pass validation and still land its stale write\n/// AFTER the reap's clears. Per-ENDPOINT, so stamps to different perches never\n/// serialize (KH 7.12 stays about global locks; the fs writes here are the\n/// same bounded per-perch fsyncs the exit-waiter always did).\n// [impl->REQ-CONTROL-STAMP-CONVERGENCE]\nstruct StampSlot {\n    gen: AtomicU64,\n    write: Mutex<()>,\n}\n\nfn stamp_slot(endpoint: &str) -> Arc<StampSlot> {\n    static SLOTS: OnceLock<Mutex<HashMap<String, Arc<StampSlot>>>> = OnceLock::new();\n    let slots = SLOTS.get_or_init(|| Mutex::new(HashMap::new()));\n    let mut map = slots.lock().unwrap_or_else(|p| p.into_inner());\n    Arc::clone(map.entry(endpoint.to_string()).or_insert_with(|| {\n        Arc::new(StampSlot {\n            gen: AtomicU64::new(0),\n            write: Mutex::new(()),\n        })\n    }))\n}\n\n/// Converge a perch's control/viewer stamps to the live session truth, OFF any log\n/// lock (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). The broker stays the single writer;\n/// this closes the STAMP-BEFORE-BIND loss — a picker-created endpoint's spawn stamps\n/// `controlled=true` BEFORE the adapter binds its perch, so `mutate_info` returns\n/// `NotFound` and the edge stamp is swallowed; the adapter's bind then writes\n/// `controlled:false` and no later edge re-stamps, so the endpoint reads uncontrolled\n/// forever while driven. This DIVERGENCE-GATED re-assert (on the `KIND_SESSIONS` poll,\n/// bounded by the reconcile cadence) writes ONLY on a real difference — no fsync storm.\n/// Best-effort: an unbound / corrupt / perch-less endpoint has nowhere to converge yet.\n///\n/// `snap_gen` is the endpoint's stamp generation AT SNAPSHOT time (ADR-0041\n/// decision 4): the write is refused — loud, nothing written — when the\n/// generation has advanced since (an exit-waiter reap landed between our\n/// under-lock snapshot and this off-lock write; the snapshot is stale truth\n/// about a session that no longer exists). Validation + write run under the\n/// endpoint's stamp-write serial so the reap can never interleave the pair.\n/// Returns whether the converge APPLIED (false = stale-refused), so the seam is\n/// unit-testable.\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\n// [impl->REQ-CONTROL-STAMP-CONVERGENCE]\nfn converge_perch_stamps(\n    endpoint: &str,\n    want_driven_by: Option<&str>,\n    want_controlled: bool,\n    want_viewers: u32,\n    snap_gen: u64,\n) -> bool {\n    if endpoint.is_empty() {\n        return false;\n    }\n    let slot = stamp_slot(endpoint);\n    let _write = slot.write.lock().unwrap_or_else(|p| p.into_inner());\n    if slot.gen.load(Ordering::Acquire) != snap_gen {\n        eprintln!(\n            \"STAMP_STALE_SKIP:{endpoint}: pre-reap sessions snapshot refused (stamp gen advanced)\"\n        );\n        return false;\n    }\n    let perch = resolve_perch_path(endpoint, ParentHint::Infer);\n    let Some(rec) = spt_store::info::read_info(&perch) else {\n        return false; // perch not bound yet (the pre-bind window) / corrupt — nothing to gate on\n    };\n    let (control, viewer) = stamp_divergence(\n        rec.driven_by.as_deref(),\n        rec.controlled,\n        rec.viewer_count.unwrap_or(0),\n        want_driven_by,\n        want_controlled,\n        want_viewers,\n    );\n    if control {\n        let _ = spt_store::info::set_driven_by(&perch, want_driven_by);\n        let _ = spt_store::info::set_controlled(&perch, want_controlled);\n    }\n    if viewer {\n        let _ = spt_store::info::set_viewer_count(&perch, want_viewers);\n    }\n    true\n}\n\nimpl OutputLog {\n    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {\n        OutputLog {\n            session_id,\n            next_seq: 0,\n            ring: VecDeque::new(),\n            cap_chunks,\n            controller: None,\n            controller_epoch: Arc::new(AtomicU64::new(0)),\n            viewers: HashMap::new(),\n            next_viewer_id: 0,\n            endpoint,\n            size,\n            delivered_through: Arc::new(AtomicU64::new(0)),\n            grid: ScreenGrid::new(size.0, size.1),\n        }\n    }\n\n    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\n    /// carrying the current screen at pseudo-seq `next_seq - 1`.\n    ///\n    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\n    /// --attach` case #6 is about (attach to an already-producing TUI). A\n    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\n    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\n    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\n    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\n    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\n    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\n    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\n        if from_seq != 0 || self.next_seq == 0 {\n            return None;\n        }\n        Some((self.next_seq - 1, self.grid.render_repaint()))\n    }\n\n    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\n    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\n    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\n    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\n    /// or its writer is gone) so the drain can `mark_controller_gone(epoch)` OFF the\n    /// log lock; `None` otherwise.\n    ///\n    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the\n    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\n    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\n    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\n    /// than the PTY floods filled its channel, the inline poll throttled the drain,\n    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\n    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\n    /// exactly like a viewer: the drain runs at full PTY speed regardless of\n    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\n    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\n    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\n    /// (bounded wedge preserved). The writer still does the blocking socket write +\n    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\n    /// single drain thread.\n    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\n    #[must_use]\n    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\n    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\n    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\n    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\n    /// risks garbage to a subscriber. Cheap-check the ring invariants (counts within\n    /// cap, indices ordered and below `next_seq`); on ANY violation RESET the ring\n    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\n    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\n    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\n    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\n    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\n    fn clamp_or_reset(&mut self) -> bool {\n        let within_cap = self.ring.len() <= self.cap_chunks;\n        let last_below_next = match self.ring.back() {\n            Some(&(last, _)) => last < self.next_seq,\n            None => true,\n        };\n        let ordered = match (self.ring.front(), self.ring.back()) {\n            (Some(&(front, _)), Some(&(back, _))) => front <= back,\n            _ => true,\n        };\n        if within_cap && last_below_next && ordered {\n            return false;\n        }\n        let had = self.ring.len();\n        self.ring.clear();\n        eprintln!(\n            \"broker: session {} output ring was torn by a poison-recover \\\n             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\n             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\n            self.session_id, self.next_seq\n        );\n        true\n    }\n\n    /// The current high-water sequence (the next seq to be appended) — the snapshot\n    /// point for the inject leg's settle-gate + echo-verify (REQ-INJECT-MULTILINE-\n    /// INTEGRITY). A rising high-water since a baseline == the PTY produced output.\n    fn high_water(&self) -> u64 {\n        self.next_seq\n    }\n\n    /// Concatenate ring bytes at or after `from_seq` — the output PRODUCED since a\n    /// snapshot, used to confirm a settle probe echoed (readiness, Layer 1) and that\n    /// a typed payload's head echoed back (integrity, Layer 2). Best-effort: bytes\n    /// pruned out of the bounded ring are simply absent (the echo we look for is\n    /// recent, well within cap).\n    fn bytes_since(&self, from_seq: u64) -> Vec<u8> {\n        let mut out = Vec::new();\n        for (seq, chunk) in self.ring.iter() {\n            if *seq >= from_seq {\n                out.extend_from_slice(chunk);\n            }\n        }\n        out\n    }\n\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\n        let seq = self.next_seq;\n        self.next_seq += 1;\n        self.ring.push_back((seq, bytes.to_vec()));\n        while self.ring.len() > self.cap_chunks {\n            self.ring.pop_front();\n        }\n        let frame = output_envelope(self.session_id, seq, bytes);\n        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\n        if !self.viewers.is_empty() {\n            let mut evicted: Vec<u64> = Vec::new();\n            for (vid, sink) in self.viewers.iter() {\n                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\n                    evicted.push(*vid);\n                }\n            }\n            if !evicted.is_empty() {\n                for vid in evicted {\n                    // Mark eviction BEFORE dropping the sink: the writer's\n                    // `rx.recv()` then returns Err (tx dropped) and reads this flag\n                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the\n                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\n                    if let Some(sink) = self.viewers.remove(&vid) {\n                        sink.evicted.store(true, Ordering::Release);\n                    }\n                }\n                self.stamp_viewer_count();\n            }\n        }\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\n        // slow controller falls behind the ring) unless it has been Full for the\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\n        let now = Instant::now();\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\n        let result = if let Some(c) = self.controller.as_mut() {\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\n                Ok(()) => {\n                    c.last_ok = now;\n                    None\n                }\n                Err(TrySendError::Full(_)) => {\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\n                        Some(epoch) // stalled past the deadline → bounded detach\n                    } else {\n                        None // drop this chunk; controller falls behind the ring\n                    }\n                }\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\n            }\n        } else {\n            None\n        };\n        // Advance the render grid AFTER the fan-out — it must never sit between the\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\n        // snapshots it under the same short log lock only when an attach requests it.\n        self.grid.advance(bytes);\n        result\n    }\n\n    /// The current controller's identity (`Some(node)` remote / `None` local),\n    /// or `None` when undriven. The `by`-keyed ownership logic in\n    /// [`resolve_subscribe`] reads this.\n    fn controller_by(&self) -> Option<&str> {\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\n    }\n\n    /// Whether ANY controller currently drives this session (the `controlled`\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\n    /// the log lock, then converged onto the perch OFF the lock.\n    fn has_controller(&self) -> bool {\n        self.controller.is_some()\n    }\n\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\n    /// for the convergence re-assert.\n    fn live_viewer_count(&self) -> u32 {\n        self.viewers.len() as u32\n    }\n\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\n    ///\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\n    /// inline under the log lock. The writer thread advances the authoritative\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\n    /// single live writer per connection and a monotonic on-wire seq stream,\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\n    /// controller's identity.\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64, attach_gen: u64) {\n        // Drop the prior controller sink first (its writer's live loop ends when\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\n        // for the old controller can't unseat this one and (b) a prior writer\n        // still flushing its initial batch returns before its next frame.\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\n        // successor, and the new conn accretes the controller role fact.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if let Some(old) = self.controller.take() {\n            old.send.lifecycle_event(\n                \"controller-replaced\",\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\n            );\n        }\n        sub.describe(&format!(\n            \"controller session={} endpoint={} by={}\",\n            self.session_id,\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\n            by.as_deref().unwrap_or(\"local\")\n        ));\n        sub.lifecycle_event(\n            \"controller-attach\",\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\n        );\n        self.controller = None;\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\n        // batch is owned by the writer thread (never pushed through the live channel —\n        // it must not block the drain); live frames stream raw + in-order afterwards.\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\n            Some(repaint) => vec![repaint],\n            None => self\n                .ring\n                .iter()\n                .filter(|(s, _)| *s >= from_seq)\n                .cloned()\n                .collect(),\n        };\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n        let writer_send = Arc::clone(&sub);\n        let dt = Arc::clone(&self.delivered_through);\n        let epoch_gate = Arc::clone(&self.controller_epoch);\n        let sid = self.session_id;\n        // W2 decouple: the writer publishes its in-flight-write window here so a\n        // stall-evict can distinguish a wedged writer from an idle one.\n        let write_blocked_since = Arc::new(Mutex::new(None));\n        let writer_blocked = Arc::clone(&write_blocked_since);\n        let writer = thread::spawn(move || {\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\n        });\n        self.controller = Some(ControllerSink {\n            tx,\n            send: sub,\n            by,\n            last_ok: Instant::now(),\n            write_blocked_since,\n            _writer: writer,\n            attach_gen,\n            });\n        self.stamp_driven_by();\n    }\n\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\n    fn clear_controller(&mut self) {\n        if self.controller.take().is_some() {\n            self.stamp_driven_by();\n        }\n    }\n\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\n    /// controller) it is NOT finished, so a live controller is never touched (the\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\n    /// EXISTING [`converge_perch_stamps`] clears the now-stale info.json\n    /// `driven_by`/`controlled` stamp — no new detach path, no new query.\n    ///\n    /// LOCK-SAFE: this runs UNDER the log lock in the snapshot closure, so it must do\n    /// NO file I/O (the lock-across-effect discipline, KH 7.12/5.16). It drops the\n    /// sink in-memory ONLY (like [`become_controller`]'s prior-drop) and does NOT\n    /// [`stamp_driven_by`] — the OFF-lock `converge_perch_stamps` writes the honest\n    /// stamp. Returns true iff it reaped.\n    ///\n    /// SCOPE (doyle B-2 reduction): catches a writer that FAILED A WRITE (output was\n    /// flowing when the conn severed) or whose detach was dropped by a prior wedge\n    /// (the B-1 poison class). A TRULY IDLE severed controller (writer parked on recv,\n    /// no output, conn not yet EOF'd) stays `is_finished() == false` and converges\n    /// only once output resumes or the conn EOFs (→ `handle_conn` detach) — that\n    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\n…\n}\n\n[Showing lines 619-1103 of 6771. Use :1104 to continue]"}],"details":{"truncation":{"content":"    /// observed and only detaches if it still matches — so a slow-controller\r\n    /// timeout can never unseat a NEWER controller that attached meanwhile. The\r\n    /// writer snapshots its own epoch and returns the moment this value moves past\r\n    /// it, so a SUPERSEDED writer flushes no further frames (P1c, single live\r\n    /// writer per connection, [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\r\n    controller_epoch: Arc<AtomicU64>,\r\n    /// The read-only viewers, keyed by a broker-minted viewer id.\r\n    viewers: HashMap<u64, ViewerSink>,\r\n    /// Monotonic viewer-id source.\r\n    next_viewer_id: u64,\r\n    /// The owning endpoint (the spawn's label; empty = unlabeled / raw test).\r\n    /// The broker is the SINGLE writer of `driven_by` + `viewer_count` on this\r\n    /// perch (REQ-RCVIEW-1, resolves the clear-race); empty ⇒ nowhere to stamp.\r\n    endpoint: String,\r\n    /// Last-known PTY size — seeded at spawn, updated on a controller resize.\r\n    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox\r\n    /// feed); the broker holds it since the PTY exposes no size getter.\r\n    size: (u16, u16),\r\n    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a\r\n    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**\r\n    /// to the controller, +1). It is **log** state, not subscriber state, so it\r\n    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and\r\n    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead\r\n    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**\r\n    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).\r\n    ///\r\n    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**\r\n    /// (never decreases) — see [`advanced_cursor`]. Both invariants are\r\n    /// load-bearing for at-least-once output: advancing past a *failed* write to a\r\n    /// dying-but-not-yet-detached controller would make resume skip that chunk\r\n    /// forever (at-least-once → at-most-once); decreasing on a deliberate\r\n    /// `attach(sub, 0)` rewind-replay would reset the resume point under a live\r\n    /// reader.\r\n    ///\r\n    /// W1: now an `Arc<AtomicU64>` shared with the controller's writer thread,\r\n    /// which advances it (via [`advanced_cursor`], CAS-monotonic) as it writes —\r\n    /// the cursor moved off the drain thread with the controller delivery itself.\r\n    delivered_through: Arc<AtomicU64>,\r\n    /// The server-side **render grid** (#6 / REQ-BROKER-SCREEN-GRID, ADR-0031):\r\n    /// the authoritative CURRENT SCREEN, interpreted from the same byte stream the\r\n    /// ring records. Fed on the drain path in [`append`], AFTER the fan-out (so it\r\n    /// never adds latency to controller/viewer delivery — hazard 7.12). A cold\r\n    /// attach's initial batch is a single synthesized [`ScreenGrid::render_repaint`]\r\n    /// instead of the raw ring, so replaying an alt-screen TUI into a fresh terminal\r\n    /// no longer corrupts scrollback. Single-writer (the one drain thread), mirroring\r\n    /// the ring's single producer.\r\n    grid: ScreenGrid,\r\n}\r\n\r\n/// The post-write resume cursor (pure — the D4-1 unit kernel, mirroring\r\n/// `brainproc::consumer_gate`/`next_backoff`): advance to `seq + 1` **only** on a\r\n/// successful write, and **monotonically** (never below `current`). A failed\r\n/// write (`write_ok == false`) leaves the cursor untouched, and a re-send of an\r\n/// already-delivered `seq` (an `attach(sub, 0)` rewind, `seq + 1 <= current`)\r\n/// cannot lower it. This is the exact decision the `let _ = write_frame(...)`\r\n/// swallow used to drop (doyle D4-1 amendments 1–3).\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn advanced_cursor(current: u64, seq: u64, write_ok: bool) -> u64 {\r\n    if write_ok {\r\n        current.max(seq + 1)\r\n    } else {\r\n        current\r\n    }\r\n}\r\n\r\n/// Pure divergence decision for the control/viewer stamp convergence\r\n/// (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): does the perch's RECORDED\r\n/// (`driven_by`, `controlled`, `viewer_count`) differ from the live session TRUTH?\r\n/// Returns `(control_write_needed, viewer_write_needed)` — the divergence gate that\r\n/// keeps the per-poll re-assert from writing (fsync) when nothing changed.\r\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\nfn stamp_divergence(\r\n    rec_driven_by: Option<&str>,\r\n    rec_controlled: bool,\r\n    rec_viewers: u32,\r\n    want_driven_by: Option<&str>,\r\n    want_controlled: bool,\r\n    want_viewers: u32,\r\n) -> (bool, bool) {\r\n    let control = rec_driven_by != want_driven_by || rec_controlled != want_controlled;\r\n    let viewer = rec_viewers != want_viewers;\r\n    (control, viewer)\r\n}\r\n\r\n/// Per-endpoint stamp-transition serial (ADR-0041 decision 4,\r\n/// REQ-CONTROL-STAMP-CONVERGENCE): the generation counter a definitive\r\n/// no-session truth (the exit-waiter reap) BUMPS, and a poll-derived stamp\r\n/// write VALIDATES against — so a `KIND_SESSIONS` snapshot taken PRE-reap can\r\n/// never relatch `controlled=true` after the reap cleared it (the session is\r\n/// gone; no later poll would ever heal the latch). The `write` mutex\r\n/// serializes the validate-then-write pair against the bump-then-clear pair —\r\n/// without it the poll could pass validation and still land its stale write\r\n/// AFTER the reap's clears. Per-ENDPOINT, so stamps to different perches never\r\n/// serialize (KH 7.12 stays about global locks; the fs writes here are the\r\n/// same bounded per-perch fsyncs the exit-waiter always did).\r\n// [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\nstruct StampSlot {\r\n    gen: AtomicU64,\r\n    write: Mutex<()>,\r\n}\r\n\r\nfn stamp_slot(endpoint: &str) -> Arc<StampSlot> {\r\n    static SLOTS: OnceLock<Mutex<HashMap<String, Arc<StampSlot>>>> = OnceLock::new();\r\n    let slots = SLOTS.get_or_init(|| Mutex::new(HashMap::new()));\r\n    let mut map = slots.lock().unwrap_or_else(|p| p.into_inner());\r\n    Arc::clone(map.entry(endpoint.to_string()).or_insert_with(|| {\r\n        Arc::new(StampSlot {\r\n            gen: AtomicU64::new(0),\r\n            write: Mutex::new(()),\r\n        })\r\n    }))\r\n}\r\n\r\n/// Converge a perch's control/viewer stamps to the live session truth, OFF any log\r\n/// lock (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). The broker stays the single writer;\r\n/// this closes the STAMP-BEFORE-BIND loss — a picker-created endpoint's spawn stamps\r\n/// `controlled=true` BEFORE the adapter binds its perch, so `mutate_info` returns\r\n/// `NotFound` and the edge stamp is swallowed; the adapter's bind then writes\r\n/// `controlled:false` and no later edge re-stamps, so the endpoint reads uncontrolled\r\n/// forever while driven. This DIVERGENCE-GATED re-assert (on the `KIND_SESSIONS` poll,\r\n/// bounded by the reconcile cadence) writes ONLY on a real difference — no fsync storm.\r\n/// Best-effort: an unbound / corrupt / perch-less endpoint has nowhere to converge yet.\r\n///\r\n/// `snap_gen` is the endpoint's stamp generation AT SNAPSHOT time (ADR-0041\r\n/// decision 4): the write is refused — loud, nothing written — when the\r\n/// generation has advanced since (an exit-waiter reap landed between our\r\n/// under-lock snapshot and this off-lock write; the snapshot is stale truth\r\n/// about a session that no longer exists). Validation + write run under the\r\n/// endpoint's stamp-write serial so the reap can never interleave the pair.\r\n/// Returns whether the converge APPLIED (false = stale-refused), so the seam is\r\n/// unit-testable.\r\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n// [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\nfn converge_perch_stamps(\r\n    endpoint: &str,\r\n    want_driven_by: Option<&str>,\r\n    want_controlled: bool,\r\n    want_viewers: u32,\r\n    snap_gen: u64,\r\n) -> bool {\r\n    if endpoint.is_empty() {\r\n        return false;\r\n    }\r\n    let slot = stamp_slot(endpoint);\r\n    let _write = slot.write.lock().unwrap_or_else(|p| p.into_inner());\r\n    if slot.gen.load(Ordering::Acquire) != snap_gen {\r\n        eprintln!(\r\n            \"STAMP_STALE_SKIP:{endpoint}: pre-reap sessions snapshot refused (stamp gen advanced)\"\r\n        );\r\n        return false;\r\n    }\r\n    let perch = resolve_perch_path(endpoint, ParentHint::Infer);\r\n    let Some(rec) = spt_store::info::read_info(&perch) else {\r\n        return false; // perch not bound yet (the pre-bind window) / corrupt — nothing to gate on\r\n    };\r\n    let (control, viewer) = stamp_divergence(\r\n        rec.driven_by.as_deref(),\r\n        rec.controlled,\r\n        rec.viewer_count.unwrap_or(0),\r\n        want_driven_by,\r\n        want_controlled,\r\n        want_viewers,\r\n    );\r\n    if control {\r\n        let _ = spt_store::info::set_driven_by(&perch, want_driven_by);\r\n        let _ = spt_store::info::set_controlled(&perch, want_controlled);\r\n    }\r\n    if viewer {\r\n        let _ = spt_store::info::set_viewer_count(&perch, want_viewers);\r\n    }\r\n    true\r\n}\r\n\r\nimpl OutputLog {\r\n    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {\r\n        OutputLog {\r\n            session_id,\r\n            next_seq: 0,\r\n            ring: VecDeque::new(),\r\n            cap_chunks,\r\n            controller: None,\r\n            controller_epoch: Arc::new(AtomicU64::new(0)),\r\n            viewers: HashMap::new(),\r\n            next_viewer_id: 0,\r\n            endpoint,\r\n            size,\r\n            delivered_through: Arc::new(AtomicU64::new(0)),\r\n            grid: ScreenGrid::new(size.0, size.1),\r\n        }\r\n    }\r\n\r\n    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\r\n    /// carrying the current screen at pseudo-seq `next_seq - 1`.\r\n    ///\r\n    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\r\n    /// --attach` case #6 is about (attach to an already-producing TUI). A\r\n    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\r\n    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\r\n    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\r\n    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\r\n    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\r\n    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\r\n    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\r\n        if from_seq != 0 || self.next_seq == 0 {\r\n            return None;\r\n        }\r\n        Some((self.next_seq - 1, self.grid.render_repaint()))\r\n    }\r\n\r\n    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\r\n    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\r\n    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\r\n    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\r\n    /// or its writer is gone) so the drain can `mark_controller_gone(epoch)` OFF the\r\n    /// log lock; `None` otherwise.\r\n    ///\r\n    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the\r\n    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\r\n    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\r\n    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\r\n    /// than the PTY floods filled its channel, the inline poll throttled the drain,\r\n    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\r\n    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\r\n    /// exactly like a viewer: the drain runs at full PTY speed regardless of\r\n    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\r\n    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\r\n    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\r\n    /// (bounded wedge preserved). The writer still does the blocking socket write +\r\n    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\r\n    /// single drain thread.\r\n    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\r\n    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\r\n    #[must_use]\r\n    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\r\n    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\r\n    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\r\n    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\r\n    /// risks garbage to a subscriber. Cheap-check the ring invariants (counts within\r\n    /// cap, indices ordered and below `next_seq`); on ANY violation RESET the ring\r\n    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\r\n    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\r\n    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\r\n    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\r\n    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\r\n    fn clamp_or_reset(&mut self) -> bool {\r\n        let within_cap = self.ring.len() <= self.cap_chunks;\r\n        let last_below_next = match self.ring.back() {\r\n            Some(&(last, _)) => last < self.next_seq,\r\n            None => true,\r\n        };\r\n        let ordered = match (self.ring.front(), self.ring.back()) {\r\n            (Some(&(front, _)), Some(&(back, _))) => front <= back,\r\n            _ => true,\r\n        };\r\n        if within_cap && last_below_next && ordered {\r\n            return false;\r\n        }\r\n        let had = self.ring.len();\r\n        self.ring.clear();\r\n        eprintln!(\r\n            \"broker: session {} output ring was torn by a poison-recover \\\r\n             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\r\n             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\r\n            self.session_id, self.next_seq\r\n        );\r\n        true\r\n    }\r\n\r\n    /// The current high-water sequence (the next seq to be appended) — the snapshot\r\n    /// point for the inject leg's settle-gate + echo-verify (REQ-INJECT-MULTILINE-\r\n    /// INTEGRITY). A rising high-water since a baseline == the PTY produced output.\r\n    fn high_water(&self) -> u64 {\r\n        self.next_seq\r\n    }\r\n\r\n    /// Concatenate ring bytes at or after `from_seq` — the output PRODUCED since a\r\n    /// snapshot, used to confirm a settle probe echoed (readiness, Layer 1) and that\r\n    /// a typed payload's head echoed back (integrity, Layer 2). Best-effort: bytes\r\n    /// pruned out of the bounded ring are simply absent (the echo we look for is\r\n    /// recent, well within cap).\r\n    fn bytes_since(&self, from_seq: u64) -> Vec<u8> {\r\n        let mut out = Vec::new();\r\n        for (seq, chunk) in self.ring.iter() {\r\n            if *seq >= from_seq {\r\n                out.extend_from_slice(chunk);\r\n            }\r\n        }\r\n        out\r\n    }\r\n\r\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\r\n        let seq = self.next_seq;\r\n        self.next_seq += 1;\r\n        self.ring.push_back((seq, bytes.to_vec()));\r\n        while self.ring.len() > self.cap_chunks {\r\n            self.ring.pop_front();\r\n        }\r\n        let frame = output_envelope(self.session_id, seq, bytes);\r\n        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\r\n        if !self.viewers.is_empty() {\r\n            let mut evicted: Vec<u64> = Vec::new();\r\n            for (vid, sink) in self.viewers.iter() {\r\n                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\r\n                    evicted.push(*vid);\r\n                }\r\n            }\r\n            if !evicted.is_empty() {\r\n                for vid in evicted {\r\n                    // Mark eviction BEFORE dropping the sink: the writer's\r\n                    // `rx.recv()` then returns Err (tx dropped) and reads this flag\r\n                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the\r\n                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\r\n                    if let Some(sink) = self.viewers.remove(&vid) {\r\n                        sink.evicted.store(true, Ordering::Release);\r\n                    }\r\n                }\r\n                self.stamp_viewer_count();\r\n            }\r\n        }\r\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\r\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\r\n        // slow controller falls behind the ring) unless it has been Full for the\r\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\r\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\r\n        let now = Instant::now();\r\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\r\n        let result = if let Some(c) = self.controller.as_mut() {\r\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\r\n                Ok(()) => {\r\n                    c.last_ok = now;\r\n                    None\r\n                }\r\n                Err(TrySendError::Full(_)) => {\r\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\r\n                        Some(epoch) // stalled past the deadline → bounded detach\r\n                    } else {\r\n                        None // drop this chunk; controller falls behind the ring\r\n                    }\r\n                }\r\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\r\n            }\r\n        } else {\r\n            None\r\n        };\r\n        // Advance the render grid AFTER the fan-out — it must never sit between the\r\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\r\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\r\n        // snapshots it under the same short log lock only when an attach requests it.\r\n        self.grid.advance(bytes);\r\n        result\r\n    }\r\n\r\n    /// The current controller's identity (`Some(node)` remote / `None` local),\r\n    /// or `None` when undriven. The `by`-keyed ownership logic in\r\n    /// [`resolve_subscribe`] reads this.\r\n    fn controller_by(&self) -> Option<&str> {\r\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\r\n    }\r\n\r\n    /// Whether ANY controller currently drives this session (the `controlled`\r\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\r\n    /// the log lock, then converged onto the perch OFF the lock.\r\n    fn has_controller(&self) -> bool {\r\n        self.controller.is_some()\r\n    }\r\n\r\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\r\n    /// for the convergence re-assert.\r\n    fn live_viewer_count(&self) -> u32 {\r\n        self.viewers.len() as u32\r\n    }\r\n\r\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\r\n    ///\r\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\r\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\r\n    /// inline under the log lock. The writer thread advances the authoritative\r\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\r\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\r\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\r\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\r\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\r\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\r\n    /// single live writer per connection and a monotonic on-wire seq stream,\r\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\r\n    /// controller's identity.\r\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64, attach_gen: u64) {\r\n        // Drop the prior controller sink first (its writer's live loop ends when\r\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\r\n        // for the old controller can't unseat this one and (b) a prior writer\r\n        // still flushing its initial batch returns before its next frame.\r\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\r\n        // successor, and the new conn accretes the controller role fact.\r\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        if let Some(old) = self.controller.take() {\r\n            old.send.lifecycle_event(\r\n                \"controller-replaced\",\r\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\r\n            );\r\n        }\r\n        sub.describe(&format!(\r\n            \"controller session={} endpoint={} by={}\",\r\n            self.session_id,\r\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\r\n            by.as_deref().unwrap_or(\"local\")\r\n        ));\r\n        sub.lifecycle_event(\r\n            \"controller-attach\",\r\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\r\n        );\r\n        self.controller = None;\r\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\r\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\r\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\r\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\r\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\r\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\r\n        // batch is owned by the writer thread (never pushed through the live channel —\r\n        // it must not block the drain); live frames stream raw + in-order afterwards.\r\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\r\n            Some(repaint) => vec![repaint],\r\n            None => self\r\n                .ring\r\n                .iter()\r\n                .filter(|(s, _)| *s >= from_seq)\r\n                .cloned()\r\n                .collect(),\r\n        };\r\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\r\n        let writer_send = Arc::clone(&sub);\r\n        let dt = Arc::clone(&self.delivered_through);\r\n        let epoch_gate = Arc::clone(&self.controller_epoch);\r\n        let sid = self.session_id;\r\n        // W2 decouple: the writer publishes its in-flight-write window here so a\r\n        // stall-evict can distinguish a wedged writer from an idle one.\r\n        let write_blocked_since = Arc::new(Mutex::new(None));\r\n        let writer_blocked = Arc::clone(&write_blocked_since);\r\n        let writer = thread::spawn(move || {\r\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\r\n        });\r\n        self.controller = Some(ControllerSink {\r\n            tx,\r\n            send: sub,\r\n            by,\r\n            last_ok: Instant::now(),\r\n            write_blocked_since,\r\n            _writer: writer,\r\n            attach_gen,\r\n            });\r\n        self.stamp_driven_by();\r\n    }\r\n\r\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\r\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\r\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\r\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\r\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\r\n    fn clear_controller(&mut self) {\r\n        if self.controller.take().is_some() {\r\n            self.stamp_driven_by();\r\n        }\r\n    }\r\n\r\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\r\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\r\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\r\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\r\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\r\n    /// controller) it is NOT finished, so a live controller is never touched (the\r\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\r\n    /// EXISTING [`converge_perch_stamps`] clears the now-stale info.json\r\n    /// `driven_by`/`controlled` stamp — no new detach path, no new query.\r\n    ///\r\n    /// LOCK-SAFE: this runs UNDER the log lock in the snapshot closure, so it must do\r\n    /// NO file I/O (the lock-across-effect discipline, KH 7.12/5.16). It drops the\r\n    /// sink in-memory ONLY (like [`become_controller`]'s prior-drop) and does NOT\r\n    /// [`stamp_driven_by`] — the OFF-lock `converge_perch_stamps` writes the honest\r\n    /// stamp. Returns true iff it reaped.\r\n    ///\r\n    /// SCOPE (doyle B-2 reduction): catches a writer that FAILED A WRITE (output was\r\n    /// flowing when the conn severed) or whose detach was dropped by a prior wedge\r\n    /// (the B-1 poison class). A TRULY IDLE severed controller (writer parked on recv,\r\n    /// no output, conn not yet EOF'd) stays `is_finished() == false` and converges\r\n    /// only once output resumes or the conn EOFs (→ `handle_conn` detach) — that\r\n    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\r","truncated":true,"truncatedBy":"lines","totalLines":6153,"totalBytes":25616,"outputLines":485,"outputBytes":25616,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"struct OutputLog {\n…\n    /// observed and only detaches if it still matches — so a slow-controller\r\n    /// timeout can never unseat a NEWER controller that attached meanwhile. The\r\n    /// writer snapshots its own epoch and returns the moment this value moves past\r\n    /// it, so a SUPERSEDED writer flushes no further frames (P1c, single live\r\n    /// writer per connection, [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).\r\n    controller_epoch: Arc<AtomicU64>,\r\n    /// The read-only viewers, keyed by a broker-minted viewer id.\r\n    viewers: HashMap<u64, ViewerSink>,\r\n    /// Monotonic viewer-id source.\r\n    next_viewer_id: u64,\r\n    /// The owning endpoint (the spawn's label; empty = unlabeled / raw test).\r\n    /// The broker is the SINGLE writer of `driven_by` + `viewer_count` on this\r\n    /// perch (REQ-RCVIEW-1, resolves the clear-race); empty ⇒ nowhere to stamp.\r\n    endpoint: String,\r\n    /// Last-known PTY size — seeded at spawn, updated on a controller resize.\r\n    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox\r\n    /// feed); the broker holds it since the PTY exposes no size getter.\r\n    size: (u16, u16),\r\n    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a\r\n    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**\r\n    /// to the controller, +1). It is **log** state, not subscriber state, so it\r\n    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and\r\n    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead\r\n    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**\r\n    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).\r\n    ///\r\n    /// **Advanced only on a successful socket write** (`Ok`), and **monotonic**\r\n    /// (never decreases) — see [`advanced_cursor`]. Both invariants are\r\n    /// load-bearing for at-least-once output: advancing past a *failed* write to a\r\n    /// dying-but-not-yet-detached controller would make resume skip that chunk\r\n    /// forever (at-least-once → at-most-once); decreasing on a deliberate\r\n    /// `attach(sub, 0)` rewind-replay would reset the resume point under a live\r\n    /// reader.\r\n    ///\r\n    /// W1: now an `Arc<AtomicU64>` shared with the controller's writer thread,\r\n    /// which advances it (via [`advanced_cursor`], CAS-monotonic) as it writes —\r\n    /// the cursor moved off the drain thread with the controller delivery itself.\r\n    delivered_through: Arc<AtomicU64>,\r\n    /// The server-side **render grid** (#6 / REQ-BROKER-SCREEN-GRID, ADR-0031):\r\n    /// the authoritative CURRENT SCREEN, interpreted from the same byte stream the\r\n    /// ring records. Fed on the drain path in [`append`], AFTER the fan-out (so it\r\n    /// never adds latency to controller/viewer delivery — hazard 7.12). A cold\r\n    /// attach's initial batch is a single synthesized [`ScreenGrid::render_repaint`]\r\n    /// instead of the raw ring, so replaying an alt-screen TUI into a fresh terminal\r\n    /// no longer corrupts scrollback. Single-writer (the one drain thread), mirroring\r\n    /// the ring's single producer.\r\n    grid: ScreenGrid,\r\n}\r\n\r\n/// The post-write resume cursor (pure — the D4-1 unit kernel, mirroring\r\n/// `brainproc::consumer_gate`/`next_backoff`): advance to `seq + 1` **only** on a\r\n/// successful write, and **monotonically** (never below `current`). A failed\r\n/// write (`write_ok == false`) leaves the cursor untouched, and a re-send of an\r\n/// already-delivered `seq` (an `attach(sub, 0)` rewind, `seq + 1 <= current`)\r\n/// cannot lower it. This is the exact decision the `let _ = write_frame(...)`\r\n/// swallow used to drop (doyle D4-1 amendments 1–3).\r\n// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\nfn advanced_cursor(current: u64, seq: u64, write_ok: bool) -> u64 {\r\n    if write_ok {\r\n        current.max(seq + 1)\r\n    } else {\r\n        current\r\n    }\r\n}\r\n\r\n/// Pure divergence decision for the control/viewer stamp convergence\r\n/// (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): does the perch's RECORDED\r\n/// (`driven_by`, `controlled`, `viewer_count`) differ from the live session TRUTH?\r\n/// Returns `(control_write_needed, viewer_write_needed)` — the divergence gate that\r\n/// keeps the per-poll re-assert from writing (fsync) when nothing changed.\r\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\nfn stamp_divergence(\r\n    rec_driven_by: Option<&str>,\r\n    rec_controlled: bool,\r\n    rec_viewers: u32,\r\n    want_driven_by: Option<&str>,\r\n    want_controlled: bool,\r\n    want_viewers: u32,\r\n) -> (bool, bool) {\r\n    let control = rec_driven_by != want_driven_by || rec_controlled != want_controlled;\r\n    let viewer = rec_viewers != want_viewers;\r\n    (control, viewer)\r\n}\r\n\r\n/// Per-endpoint stamp-transition serial (ADR-0041 decision 4,\r\n/// REQ-CONTROL-STAMP-CONVERGENCE): the generation counter a definitive\r\n/// no-session truth (the exit-waiter reap) BUMPS, and a poll-derived stamp\r\n/// write VALIDATES against — so a `KIND_SESSIONS` snapshot taken PRE-reap can\r\n/// never relatch `controlled=true` after the reap cleared it (the session is\r\n/// gone; no later poll would ever heal the latch). The `write` mutex\r\n/// serializes the validate-then-write pair against the bump-then-clear pair —\r\n/// without it the poll could pass validation and still land its stale write\r\n/// AFTER the reap's clears. Per-ENDPOINT, so stamps to different perches never\r\n/// serialize (KH 7.12 stays about global locks; the fs writes here are the\r\n/// same bounded per-perch fsyncs the exit-waiter always did).\r\n// [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\nstruct StampSlot {\r\n    gen: AtomicU64,\r\n    write: Mutex<()>,\r\n}\r\n\r\nfn stamp_slot(endpoint: &str) -> Arc<StampSlot> {\r\n    static SLOTS: OnceLock<Mutex<HashMap<String, Arc<StampSlot>>>> = OnceLock::new();\r\n    let slots = SLOTS.get_or_init(|| Mutex::new(HashMap::new()));\r\n    let mut map = slots.lock().unwrap_or_else(|p| p.into_inner());\r\n    Arc::clone(map.entry(endpoint.to_string()).or_insert_with(|| {\r\n        Arc::new(StampSlot {\r\n            gen: AtomicU64::new(0),\r\n            write: Mutex::new(()),\r\n        })\r\n    }))\r\n}\r\n\r\n/// Converge a perch's control/viewer stamps to the live session truth, OFF any log\r\n/// lock (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). The broker stays the single writer;\r\n/// this closes the STAMP-BEFORE-BIND loss — a picker-created endpoint's spawn stamps\r\n/// `controlled=true` BEFORE the adapter binds its perch, so `mutate_info` returns\r\n/// `NotFound` and the edge stamp is swallowed; the adapter's bind then writes\r\n/// `controlled:false` and no later edge re-stamps, so the endpoint reads uncontrolled\r\n/// forever while driven. This DIVERGENCE-GATED re-assert (on the `KIND_SESSIONS` poll,\r\n/// bounded by the reconcile cadence) writes ONLY on a real difference — no fsync storm.\r\n/// Best-effort: an unbound / corrupt / perch-less endpoint has nowhere to converge yet.\r\n///\r\n/// `snap_gen` is the endpoint's stamp generation AT SNAPSHOT time (ADR-0041\r\n/// decision 4): the write is refused — loud, nothing written — when the\r\n/// generation has advanced since (an exit-waiter reap landed between our\r\n/// under-lock snapshot and this off-lock write; the snapshot is stale truth\r\n/// about a session that no longer exists). Validation + write run under the\r\n/// endpoint's stamp-write serial so the reap can never interleave the pair.\r\n/// Returns whether the converge APPLIED (false = stale-refused), so the seam is\r\n/// unit-testable.\r\n// [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n// [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\nfn converge_perch_stamps(\r\n    endpoint: &str,\r\n    want_driven_by: Option<&str>,\r\n    want_controlled: bool,\r\n    want_viewers: u32,\r\n    snap_gen: u64,\r\n) -> bool {\r\n    if endpoint.is_empty() {\r\n        return false;\r\n    }\r\n    let slot = stamp_slot(endpoint);\r\n    let _write = slot.write.lock().unwrap_or_else(|p| p.into_inner());\r\n    if slot.gen.load(Ordering::Acquire) != snap_gen {\r\n        eprintln!(\r\n            \"STAMP_STALE_SKIP:{endpoint}: pre-reap sessions snapshot refused (stamp gen advanced)\"\r\n        );\r\n        return false;\r\n    }\r\n    let perch = resolve_perch_path(endpoint, ParentHint::Infer);\r\n    let Some(rec) = spt_store::info::read_info(&perch) else {\r\n        return false; // perch not bound yet (the pre-bind window) / corrupt — nothing to gate on\r\n    };\r\n    let (control, viewer) = stamp_divergence(\r\n        rec.driven_by.as_deref(),\r\n        rec.controlled,\r\n        rec.viewer_count.unwrap_or(0),\r\n        want_driven_by,\r\n        want_controlled,\r\n        want_viewers,\r\n    );\r\n    if control {\r\n        let _ = spt_store::info::set_driven_by(&perch, want_driven_by);\r\n        let _ = spt_store::info::set_controlled(&perch, want_controlled);\r\n    }\r\n    if viewer {\r\n        let _ = spt_store::info::set_viewer_count(&perch, want_viewers);\r\n    }\r\n    true\r\n}\r\n\r\nimpl OutputLog {\r\n    fn new(session_id: u64, cap_chunks: usize, endpoint: String, size: (u16, u16)) -> Self {\r\n        OutputLog {\r\n            session_id,\r\n            next_seq: 0,\r\n            ring: VecDeque::new(),\r\n            cap_chunks,\r\n            controller: None,\r\n            controller_epoch: Arc::new(AtomicU64::new(0)),\r\n            viewers: HashMap::new(),\r\n            next_viewer_id: 0,\r\n            endpoint,\r\n            size,\r\n            delivered_through: Arc::new(AtomicU64::new(0)),\r\n            grid: ScreenGrid::new(size.0, size.1),\r\n        }\r\n    }\r\n\r\n    /// The synthesized clean-repaint initial batch (#6, ADR-0031): a single frame\r\n    /// carrying the current screen at pseudo-seq `next_seq - 1`.\r\n    ///\r\n    /// **Scoped to a COLD attach (`from_seq == 0`)** — the `spt rc` / `endpoint run\r\n    /// --attach` case #6 is about (attach to an already-producing TUI). A\r\n    /// resume-from-floor re-fetch (`from_seq > 0`, a controller recovering a b4\r\n    /// drop-don't-block gap) is deliberately NOT repainted: it keeps the raw-ring\r\n    /// exactly-once replay + the strict reject-gap + the irrecoverable-behind\r\n    /// detection untouched (REQ-HAZARD-CONTROLLER-GAP-RESUME / B2). `None` also when\r\n    /// nothing has been produced yet (`next_seq == 0`) → an empty initial batch, so\r\n    /// the first live frame is the contiguous seq 0 (the spawn seat, unchanged).\r\n    fn repaint_initial(&self, from_seq: u64) -> Option<(u64, Vec<u8>)> {\r\n        if from_seq != 0 || self.next_seq == 0 {\r\n            return None;\r\n        }\r\n        Some((self.next_seq - 1, self.grid.render_repaint()))\r\n    }\r\n\r\n    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in\r\n    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE\r\n    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns\r\n    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,\r\n    /// or its writer is gone) so the drain can `mark_controller_gone(epoch)` OFF the\r\n    /// log lock; `None` otherwise.\r\n    ///\r\n    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the\r\n    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but\r\n    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to\r\n    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower\r\n    /// than the PTY floods filled its channel, the inline poll throttled the drain,\r\n    /// and the viewer fan-out below stopped running → a concurrent `rc --view`\r\n    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`\r\n    /// exactly like a viewer: the drain runs at full PTY speed regardless of\r\n    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind\r\n    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`\r\n    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted\r\n    /// (bounded wedge preserved). The writer still does the blocking socket write +\r\n    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's\r\n    /// single drain thread.\r\n    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\r\n    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\r\n    #[must_use]\r\n    /// Coherence clamp for a poison-RECOVERED ring (B-1, doyle ruling; run only by\r\n    /// [`recover_log`] on the recovery path). A panic mid-[`append`] can leave the\r\n    /// ring torn — an over-cap ring (interrupted prune), a last seq not below\r\n    /// `next_seq`, or a non-monotonic front/back — and serving those bytes blind\r\n    /// risks garbage to a subscriber. Cheap-check the ring invariants (counts within\r\n    /// cap, indices ordered and below `next_seq`); on ANY violation RESET the ring\r\n    /// EMPTY (+ loud log) — `next_seq` is left intact so cursors never rewind, and the\r\n    /// lost scrollback self-heals on the next PTY output + repaint. Returns `true`\r\n    /// iff it reset. A coherent ring (the common case — most panics fire OUTSIDE the\r\n    /// ring mutation, poisoning the lock while the buffer stays intact) is untouched.\r\n    // [impl->REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\r\n    fn clamp_or_reset(&mut self) -> bool {\r\n        let within_cap = self.ring.len() <= self.cap_chunks;\r\n        let last_below_next = match self.ring.back() {\r\n            Some(&(last, _)) => last < self.next_seq,\r\n            None => true,\r\n        };\r\n        let ordered = match (self.ring.front(), self.ring.back()) {\r\n            (Some(&(front, _)), Some(&(back, _))) => front <= back,\r\n            _ => true,\r\n        };\r\n        if within_cap && last_below_next && ordered {\r\n            return false;\r\n        }\r\n        let had = self.ring.len();\r\n        self.ring.clear();\r\n        eprintln!(\r\n            \"broker: session {} output ring was torn by a poison-recover \\\r\n             (had {had} chunks, next_seq {}); reset to empty — scrollback self-heals \\\r\n             on the next output [REQ-HAZARD-BROKER-FLOOR-LOCK-POISON]\",\r\n            self.session_id, self.next_seq\r\n        );\r\n        true\r\n    }\r\n\r\n    /// The current high-water sequence (the next seq to be appended) — the snapshot\r\n    /// point for the inject leg's settle-gate + echo-verify (REQ-INJECT-MULTILINE-\r\n    /// INTEGRITY). A rising high-water since a baseline == the PTY produced output.\r\n    fn high_water(&self) -> u64 {\r\n        self.next_seq\r\n    }\r\n\r\n    /// Concatenate ring bytes at or after `from_seq` — the output PRODUCED since a\r\n    /// snapshot, used to confirm a settle probe echoed (readiness, Layer 1) and that\r\n    /// a typed payload's head echoed back (integrity, Layer 2). Best-effort: bytes\r\n    /// pruned out of the bounded ring are simply absent (the echo we look for is\r\n    /// recent, well within cap).\r\n    fn bytes_since(&self, from_seq: u64) -> Vec<u8> {\r\n        let mut out = Vec::new();\r\n        for (seq, chunk) in self.ring.iter() {\r\n            if *seq >= from_seq {\r\n                out.extend_from_slice(chunk);\r\n            }\r\n        }\r\n        out\r\n    }\r\n\r\n    fn append(&mut self, bytes: &[u8]) -> Option<u64> {\r\n        let seq = self.next_seq;\r\n        self.next_seq += 1;\r\n        self.ring.push_back((seq, bytes.to_vec()));\r\n        while self.ring.len() > self.cap_chunks {\r\n            self.ring.pop_front();\r\n        }\r\n        let frame = output_envelope(self.session_id, seq, bytes);\r\n        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\r\n        if !self.viewers.is_empty() {\r\n            let mut evicted: Vec<u64> = Vec::new();\r\n            for (vid, sink) in self.viewers.iter() {\r\n                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {\r\n                    evicted.push(*vid);\r\n                }\r\n            }\r\n            if !evicted.is_empty() {\r\n                for vid in evicted {\r\n                    // Mark eviction BEFORE dropping the sink: the writer's\r\n                    // `rx.recv()` then returns Err (tx dropped) and reads this flag\r\n                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the\r\n                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).\r\n                    if let Some(sink) = self.viewers.remove(&vid) {\r\n                        sink.evicted.store(true, Ordering::Release);\r\n                    }\r\n                }\r\n                self.stamp_viewer_count();\r\n            }\r\n        }\r\n        // Controller: the SAME non-blocking discipline as a viewer (b4). One\r\n        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a\r\n        // slow controller falls behind the ring) unless it has been Full for the\r\n        // whole deadline since its last success → EVICT. Disconnected (writer gone)\r\n        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.\r\n        let now = Instant::now();\r\n        let epoch = self.controller_epoch.load(Ordering::Acquire);\r\n        let result = if let Some(c) = self.controller.as_mut() {\r\n            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {\r\n                Ok(()) => {\r\n                    c.last_ok = now;\r\n                    None\r\n                }\r\n                Err(TrySendError::Full(_)) => {\r\n                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {\r\n                        Some(epoch) // stalled past the deadline → bounded detach\r\n                    } else {\r\n                        None // drop this chunk; controller falls behind the ring\r\n                    }\r\n                }\r\n                Err(TrySendError::Disconnected(_)) => Some(epoch),\r\n            }\r\n        } else {\r\n            None\r\n        };\r\n        // Advance the render grid AFTER the fan-out — it must never sit between the\r\n        // drain and the controller/viewer `try_send` (REQ-HAZARD-INJECT-CONTROL-COEXIST\r\n        // 7.12). The grid is single-writer (this one drain thread); `render_repaint`\r\n        // snapshots it under the same short log lock only when an attach requests it.\r\n        self.grid.advance(bytes);\r\n        result\r\n    }\r\n\r\n    /// The current controller's identity (`Some(node)` remote / `None` local),\r\n    /// or `None` when undriven. The `by`-keyed ownership logic in\r\n    /// [`resolve_subscribe`] reads this.\r\n    fn controller_by(&self) -> Option<&str> {\r\n        self.controller.as_ref().and_then(|c| c.by.as_deref())\r\n    }\r\n\r\n    /// Whether ANY controller currently drives this session (the `controlled`\r\n    /// any-controller truth — REQ-HAZARD-CONTROL-STAMP-CONVERGENCE). Snapshotted under\r\n    /// the log lock, then converged onto the perch OFF the lock.\r\n    fn has_controller(&self) -> bool {\r\n        self.controller.is_some()\r\n    }\r\n\r\n    /// The live viewer count (the `viewer_count` truth). Snapshotted under the log lock\r\n    /// for the convergence re-assert.\r\n    fn live_viewer_count(&self) -> u32 {\r\n        self.viewers.len() as u32\r\n    }\r\n\r\n    /// Become (or silently re-take) the interactive controller from `from_seq`.\r\n    ///\r\n    /// W1: the ring replay is now the INITIAL BATCH of a dedicated controller\r\n    /// writer thread (symmetric with [`add_viewer`]) — it is no longer written\r\n    /// inline under the log lock. The writer thread advances the authoritative\r\n    /// `delivered_through` cursor as it writes (D4-1: only on a successful write,\r\n    /// monotonic, never the unsent tail). Dropping any prior [`ControllerSink`]\r\n    /// ends its writer's LIVE loop when `tx` drops; bumping `controller_epoch`\r\n    /// additionally makes a stale deadline-evict a no-op AND stops a superseded\r\n    /// writer mid-INITIAL-BATCH (P1c — a writer still flushing its owned `initial`\r\n    /// batch ignores the dropped `tx`, so the epoch gate is what guarantees a\r\n    /// single live writer per connection and a monotonic on-wire seq stream,\r\n    /// [`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]). Stamps `driven_by` to the new\r\n    /// controller's identity.\r\n    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64, attach_gen: u64) {\r\n        // Drop the prior controller sink first (its writer's live loop ends when\r\n        // tx drops), and bump the generation so (a) an in-flight deadline-evict\r\n        // for the old controller can't unseat this one and (b) a prior writer\r\n        // still flushing its initial batch returns before its next frame.\r\n        // Attribution (F-039 legs b+d): the replaced conn's record names its\r\n        // successor, and the new conn accretes the controller role fact.\r\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        if let Some(old) = self.controller.take() {\r\n            old.send.lifecycle_event(\r\n                \"controller-replaced\",\r\n                &format!(\"session={} new_conn={}\", self.session_id, sub.id()),\r\n            );\r\n        }\r\n        sub.describe(&format!(\r\n            \"controller session={} endpoint={} by={}\",\r\n            self.session_id,\r\n            if self.endpoint.is_empty() { \"-\" } else { &self.endpoint },\r\n            by.as_deref().unwrap_or(\"local\")\r\n        ));\r\n        sub.lifecycle_event(\r\n            \"controller-attach\",\r\n            &format!(\"session={} from_seq={from_seq}\", self.session_id),\r\n        );\r\n        self.controller = None;\r\n        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;\r\n        // #6 (ADR-0031): a COLD attach's initial batch is a single synthesized clean\r\n        // repaint of the CURRENT SCREEN (at pseudo-seq `next_seq - 1`) instead of the\r\n        // raw ring — replaying an alt-screen TUI's raw ring into a fresh terminal is\r\n        // exactly the corruption #6/#7/#8 report. A resume-from-floor (`from_seq > 0`)\r\n        // keeps the raw-ring exactly-once re-fetch untouched (B2). Either way the\r\n        // batch is owned by the writer thread (never pushed through the live channel —\r\n        // it must not block the drain); live frames stream raw + in-order afterwards.\r\n        let initial: Vec<(u64, Vec<u8>)> = match self.repaint_initial(from_seq) {\r\n            Some(repaint) => vec![repaint],\r\n            None => self\r\n                .ring\r\n                .iter()\r\n                .filter(|(s, _)| *s >= from_seq)\r\n                .cloned()\r\n                .collect(),\r\n        };\r\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\r\n        let writer_send = Arc::clone(&sub);\r\n        let dt = Arc::clone(&self.delivered_through);\r\n        let epoch_gate = Arc::clone(&self.controller_epoch);\r\n        let sid = self.session_id;\r\n        // W2 decouple: the writer publishes its in-flight-write window here so a\r\n        // stall-evict can distinguish a wedged writer from an idle one.\r\n        let write_blocked_since = Arc::new(Mutex::new(None));\r\n        let writer_blocked = Arc::clone(&write_blocked_since);\r\n        let writer = thread::spawn(move || {\r\n            controller_writer(sid, writer_send, initial, rx, dt, epoch, epoch_gate, writer_blocked)\r\n        });\r\n        self.controller = Some(ControllerSink {\r\n            tx,\r\n            send: sub,\r\n            by,\r\n            last_ok: Instant::now(),\r\n            write_blocked_since,\r\n            _writer: writer,\r\n            attach_gen,\r\n            });\r\n        self.stamp_driven_by();\r\n    }\r\n\r\n    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared\r\n    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per\r\n    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its\r\n    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an\r\n    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).\r\n    fn clear_controller(&mut self) {\r\n        if self.controller.take().is_some() {\r\n            self.stamp_driven_by();\r\n        }\r\n    }\r\n\r\n    /// B-2 (REQ-CONTROLLER-LIVENESS-REAP): lazily drop a controller whose WRITER\r\n    /// THREAD has EXITED — the record-vs-truth honesty step, run on the authoritative\r\n    /// `KIND_SESSIONS` poll BEFORE `controller_by`/`has_controller` are read. The\r\n    /// writer exits when a socket write fails (a severed / keepalive-killed controller\r\n    /// conn) or its `tx` drops; while it is parked on `rx.recv()` (a LIVE but idle\r\n    /// controller) it is NOT finished, so a live controller is never touched (the\r\n    /// no-false-reap guard). Reaping here makes the broker report honest truth so the\r\n    /// EXISTING [`converge_perch_stamps`] clears the now-stale info.json\r\n    /// `driven_by`/`controlled` stamp — no new detach path, no new query.\r\n    ///\r\n    /// LOCK-SAFE: this runs UNDER the log lock in the snapshot closure, so it must do\r\n    /// NO file I/O (the lock-across-effect discipline, KH 7.12/5.16). It drops the\r\n    /// sink in-memory ONLY (like [`become_controller`]'s prior-drop) and does NOT\r\n    /// [`stamp_driven_by`] — the OFF-lock `converge_perch_stamps` writes the honest\r\n    /// stamp. Returns true iff it reaped.\r\n    ///\r\n    /// SCOPE (doyle B-2 reduction): catches a writer that FAILED A WRITE (output was\r\n    /// flowing when the conn severed) or whose detach was dropped by a prior wedge\r\n    /// (the B-1 poison class). A TRULY IDLE severed controller (writer parked on recv,\r\n    /// no output, conn not yet EOF'd) stays `is_finished() == false` and converges\r\n    /// only once output resumes or the conn EOFs (→ `handle_conn` detach) — that\r\n    /// harder active-probe case is the reserved [`REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT`],\r\n…\n}","startLine":604,"lineNumbers":[604,null,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,null,1531]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6771,"totalBytes":25616,"outputLines":485,"outputBytes":25616,"shownRange":{"start":619,"end":1103},"nextOffset":1104}}},"isError":false,"timestamp":1784365483483}}
{"type":"message","id":"67b182a4","parentId":"0933cec1","timestamp":"2026-07-18T09:04:54.671Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing screen implementation for spacing issues**"},{"type":"toolCall","id":"call_xt70Ykjirxpu2XWUXMpLM0wn|fc_0ce5441d29b9f05d016a5b41b69bf8819bb175e03c0b54a982","name":"read","arguments":{"i":"Reading screen grid implementation","path":"crates/spt-term/src/screen.rs","selector":"1-260"}},{"type":"toolCall","id":"call_5Dxo4zqUFn3cKSkelKmAxZzc|fc_0ce5441d29b9f05d016a5b41b69c0c819bb9617e2fa593445b","name":"read","arguments":{"i":"Reading repaint renderer","path":"crates/spt-term/src/screen.rs","selector":"500-780"}},{"type":"toolCall","id":"call_ZtyT5tQlkQRxxYUylL7zRJTN|fc_0ce5441d29b9f05d016a5b41b69c40819bbf32d65069a8c4c6","name":"read","arguments":{"i":"Reading grid erase handling","path":"crates/spt-term/src/screen.rs","selector":"780-1080"}},{"type":"toolCall","id":"call_vMJqSmPjgmFSp5bIndbokdtn|fc_0ce5441d29b9f05d016a5b41b69c48819bbb5f3f2044a18ee5","name":"grep","arguments":{"i":"Locating screen operations","pattern":"fn (render_repaint|advance|clear|erase|delete|scroll|linefeed|carriage_return|csi_dispatch|esc_dispatch|execute|print|resize)|trim_end|last_non|Cell::default|b'J'|b'K'","path":"crates/spt-term/src/screen.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_GUe7BEreCuHqfYoFBbz8Vlho|fc_0ce5441d29b9f05d016a5b41b69c50819badd7a99a9f45d0bc","name":"grep","arguments":{"i":"Locating RC pump output","pattern":"run_attach_inner|run_attach_pump|stdout|write_all|flush|RawGuard|Drop for|restore_bytes|Exit|TimedOut|timeout|disconnect|Detach","path":"crates/spt/src/rc.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_PhDHrlm4TDMw29kRNgdUaQZ4|fc_0ce5441d29b9f05d016a5b41b69c54819bad69f04abd5f4fc6","name":"read","arguments":{"i":"Reading attach serve loop","path":"crates/spt-daemon/src/attach.rs","selector":"278-590"}},{"type":"toolCall","id":"call_33JIq9zLNt7gTyZKdehLGP3l|fc_0ce5441d29b9f05d016a5b41b69c5c819b9e0f2897344c032c","name":"read","arguments":{"i":"Reading broker writer cleanup","path":"crates/spt-daemon/src/broker.rs","selector":"1480-1755"}},{"type":"toolCall","id":"call_B5EA5I37N3xrJ3OuwA80hP4q|fc_0ce5441d29b9f05d016a5b41b69c60819b87bcfab8335c60c6","name":"read","arguments":{"i":"Reading broker connection cleanup","path":"crates/spt-daemon/src/broker.rs","selector":"2900-3240"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":34114,"output":436,"cacheRead":56832,"cacheWrite":0,"totalTokens":91382,"reasoningTokens":15,"cost":{"input":0.17057000000000003,"output":0.01308,"cacheRead":0.028416,"cacheWrite":0,"total":0.21206600000000003}},"stopReason":"toolUse","timestamp":1784365483526,"responseId":"resp_0ce5441d29b9f05d016a5b41ac16e0819b99cc5cd3694da487","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b41aeb7f0819b88d2f5b02574af59","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0G2lf9jY45jsczg5Zj1BZfWSSqMBBSGFgzHcyRJyyXQ82YfMhKH7GxGlppUNcMfnTN00AJY-xd3XoRZBWGd6z_uy9XbFkUUtnUcjrjdzjojq51f_cmKtb6UPOjCFsRIU7zS3-4157ildMPyqwNde-8-OBnsarCaLvWlz2f15oOBO-clkXEnlTgZq_F7g6pdJTSEgCc1JSJzZh7H1t0bqI05bSTzTF8oKFpYsjUAXP3bnobIRtrFJv92WSt8XNXGPPGNxzT1vDbPWGDl2HE8N42pPMFFCo4fBRenrxD1Rgj5hYmRxSwYYm_gx1hR5RY637OJOEvbhWaDGlGtD40OBuCmjUKQYev2grjaZQbGj2hqUrXOsqAgLtrMEi_Qex92Wt4KRPDrVk0fRb8knMGsjOmDwsL1U-1mGWOW279XbXxsezlCkqfnskWkuoBYZwbbmTpio_mCuXwhSQmZ4O4Unr3SuKqvQOq28v9pAWSSu2a0ifHWG4tHrL4OAYdqruB4LHDMpU6_IS93xavgCnYEiENl_k-EMY9LpS9uWVBl8Oc43YHgy_uKpI3aUeEEf8mJEp-Ixf1n0_mzycilAcdyuCpFZGerLyKRwyi9Ui_kPsz_-_f2eq8-vTnrfflzVxXY-GuR4xR4PXWCqce-ndxHj-QtiwqjbQ0T2spzFjh_MXb71wMzgZ8-eCZuAt9M0qlfr8FhVwGbOZXzPaZxtuvNNjwSodrkox94t6kMR_Qx6X48ER6138cirTd4GhA_fx-nGDEvpCyb5_bfcEr5Id_OOT3oJtO0b44QUAo6dR2EpKQmt0iOu4JiFdASGspFhW3LnnUV22R2W3KKroJ9gMemynA1B0A_FsAd4UF9F8P42yctYGOtY20CnnkzrU34Q9ZrfT3AdeDEfL7UpJPAbgXTH895mSwnB4AWAKOP8Wc-U6NOXM70O2AHVU-XE7qbuG2R3DKC5ZRtYrGC0WSzOwCfDGausCysxrxJU64do-1VLVEiis5M5J42MHmjRdXxbw79--c0","summary":[{"type":"summary_text","text":"**Analyzing screen implementation for spacing issues**"}]},{"id":"fc_0ce5441d29b9f05d016a5b41b69bf8819bb175e03c0b54a982","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading screen grid implementation\",\"path\":\"crates/spt-term/src/screen.rs\",\"selector\":\"1-260\"}","call_id":"call_xt70Ykjirxpu2XWUXMpLM0wn","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41b69c0c819bb9617e2fa593445b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading repaint renderer\",\"path\":\"crates/spt-term/src/screen.rs\",\"selector\":\"500-780\"}","call_id":"call_5Dxo4zqUFn3cKSkelKmAxZzc","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41b69c40819bbf32d65069a8c4c6","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading grid erase handling\",\"path\":\"crates/spt-term/src/screen.rs\",\"selector\":\"780-1080\"}","call_id":"call_ZtyT5tQlkQRxxYUylL7zRJTN","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41b69c48819bbb5f3f2044a18ee5","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating screen operations\",\"pattern\":\"fn (render_repaint|advance|clear|erase|delete|scroll|linefeed|carriage_return|csi_dispatch|esc_dispatch|execute|print|resize)|trim_end|last_non|Cell::default|b'J'|b'K'\",\"path\":\"crates/spt-term/src/screen.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_vMJqSmPjgmFSp5bIndbokdtn","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41b69c50819badd7a99a9f45d0bc","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating RC pump output\",\"pattern\":\"run_attach_inner|run_attach_pump|stdout|write_all|flush|RawGuard|Drop for|restore_bytes|Exit|TimedOut|timeout|disconnect|Detach\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_GUe7BEreCuHqfYoFBbz8Vlho","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41b69c54819bad69f04abd5f4fc6","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading attach serve loop\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"278-590\"}","call_id":"call_PhDHrlm4TDMw29kRNgdUaQZ4","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41b69c5c819b9e0f2897344c032c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker writer cleanup\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"1480-1755\"}","call_id":"call_33JIq9zLNt7gTyZKdehLGP3l","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41b69c60819b87bcfab8335c60c6","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker connection cleanup\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"2900-3240\"}","call_id":"call_B5EA5I37N3xrJ3OuwA80hP4q","name":"read"}]},"duration":11144.270999997854,"ttft":3212.5163999795914,"contextSnapshot":{"promptTokens":90946,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xt70Ykjirxpu2XWUXMpLM0wn|fc_0ce5441d29b9f05d016a5b41b69bf8819bb175e03c0b54a982","toolName":"read","startedAt":"2026-07-18T09:04:54.672Z","args":{"path":"crates/spt-term/src/screen.rs"},"intent":"Reading screen grid implementation"},"id":"8c02e33e","parentId":"67b182a4","timestamp":"2026-07-18T09:04:54.672Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5Dxo4zqUFn3cKSkelKmAxZzc|fc_0ce5441d29b9f05d016a5b41b69c0c819bb9617e2fa593445b","toolName":"read","startedAt":"2026-07-18T09:04:54.672Z","args":{"path":"crates/spt-term/src/screen.rs"},"intent":"Reading repaint renderer"},"id":"a94a620c","parentId":"8c02e33e","timestamp":"2026-07-18T09:04:54.672Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZtyT5tQlkQRxxYUylL7zRJTN|fc_0ce5441d29b9f05d016a5b41b69c40819bbf32d65069a8c4c6","toolName":"read","startedAt":"2026-07-18T09:04:54.672Z","args":{"path":"crates/spt-term/src/screen.rs"},"intent":"Reading grid erase handling"},"id":"3533c2ae","parentId":"a94a620c","timestamp":"2026-07-18T09:04:54.672Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vMJqSmPjgmFSp5bIndbokdtn|fc_0ce5441d29b9f05d016a5b41b69c48819bbb5f3f2044a18ee5","toolName":"grep","startedAt":"2026-07-18T09:04:54.672Z","args":{"path":"crates/spt-term/src/screen.rs"},"intent":"Locating screen operations"},"id":"efecbec9","parentId":"3533c2ae","timestamp":"2026-07-18T09:04:54.672Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GUe7BEreCuHqfYoFBbz8Vlho|fc_0ce5441d29b9f05d016a5b41b69c50819badd7a99a9f45d0bc","toolName":"grep","startedAt":"2026-07-18T09:04:54.672Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Locating RC pump output"},"id":"818079fa","parentId":"efecbec9","timestamp":"2026-07-18T09:04:54.672Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PhDHrlm4TDMw29kRNgdUaQZ4|fc_0ce5441d29b9f05d016a5b41b69c54819bad69f04abd5f4fc6","toolName":"read","startedAt":"2026-07-18T09:04:54.672Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading attach serve loop"},"id":"36000579","parentId":"818079fa","timestamp":"2026-07-18T09:04:54.672Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_33JIq9zLNt7gTyZKdehLGP3l|fc_0ce5441d29b9f05d016a5b41b69c5c819b9e0f2897344c032c","toolName":"read","startedAt":"2026-07-18T09:04:54.672Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker writer cleanup"},"id":"2228758c","parentId":"36000579","timestamp":"2026-07-18T09:04:54.672Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_B5EA5I37N3xrJ3OuwA80hP4q|fc_0ce5441d29b9f05d016a5b41b69c60819b87bcfab8335c60c6","toolName":"read","startedAt":"2026-07-18T09:04:54.672Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker connection cleanup"},"id":"3d88b15b","parentId":"2228758c","timestamp":"2026-07-18T09:04:54.672Z"}
{"type":"message","id":"f006f490","parentId":"3d88b15b","timestamp":"2026-07-18T09:04:54.680Z","message":{"role":"toolResult","toolCallId":"call_vMJqSmPjgmFSp5bIndbokdtn|fc_0ce5441d29b9f05d016a5b41b69c48819bbb5f3f2044a18ee5","toolName":"grep","content":[{"type":"text","text":" 168|    /// no latency to controller/viewer delivery (ADR-0031, REQ-HAZARD-INJECT-CONTROL-COEXIST).\n*169|    pub fn advance(&mut self, bytes: &[u8]) {\n 170|        for &b in bytes {\n 171|            // Disjoint field borrows: the parser and the state it drives are\n 172|            // separate fields of `self`, so this compiles without interior mutability.\n...\n 178|    /// top-left overlap and clamping the cursor.\n*179|    pub fn resize(&mut self, rows: u16, cols: u16) {\n 180|        self.state.resize(rows, cols);\n 181|    }\n 182|\n...\n 187|    /// raw-ring initial batch at `become_controller` / `add_viewer`.\n*188|    pub fn render_repaint(&self) -> Vec<u8> {\n 189|        self.state.render_repaint()\n 190|    }\n 191|}\n...\n 256|    /// preserved (so a status-bar erase keeps its colour).\n*257|    fn erase_pen(&self) -> Pen {\n 258|        Pen {\n 259|            bg: self.pen.bg,\n 260|            ..Pen::default()\n...\n 298|\n*299|    fn scroll_up(&mut self, n: usize) {\n 300|        let blank = self.blank();\n 301|        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n 302|        let cols = self.cols;\n...\n 311|\n*312|    fn scroll_down(&mut self, n: usize) {\n 313|        let blank = self.blank();\n 314|        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n 315|        let cols = self.cols;\n...\n 346|\n*347|    fn erase_display(&mut self, mode: u16) {\n 348|        let blank = self.blank();\n 349|        let (row, col, rows, cols) = (self.row, self.col, self.rows, self.cols);\n 350|        let buf = self.buf_mut();\n...\n 384|\n*385|    fn erase_line(&mut self, mode: u16) {\n 386|        let blank = self.blank();\n 387|        let (row, col, cols) = (self.row, self.col, self.cols);\n 388|        let buf = self.buf_mut();\n...\n 407|\n*408|    fn erase_chars(&mut self, n: usize) {\n 409|        let blank = self.blank();\n 410|        let (row, col, cols) = (self.row, self.col, self.cols);\n 411|        let buf = self.buf_mut();\n...\n 430|\n*431|    fn delete_chars(&mut self, n: usize) {\n 432|        let blank = self.blank();\n 433|        let (row, col, cols) = (self.row, self.col, self.cols);\n 434|        let buf = self.buf_mut();\n...\n 454|\n*455|    fn delete_lines(&mut self, n: usize) {\n 456|        if self.row < self.scroll_top || self.row > self.scroll_bottom {\n 457|            return;\n 458|        }\n...\n 556|\n*557|    fn resize(&mut self, rows: u16, cols: u16) {\n 558|        let rows = rows.max(1) as usize;\n 559|        let cols = cols.max(1) as usize;\n 560|        self.main = resized_buffer(&self.main, rows, cols);\n...\n 576|\n*577|    fn render_repaint(&self) -> Vec<u8> {\n 578|        let mut out = String::new();\n 579|        // Put the client on the matching buffer so its own scrollback is never\n 580|        // touched: 1049h saves+switches+clears the alt screen; 1049l returns to main.\n...\n 674|fn blank_buffer(rows: usize, cols: usize) -> Vec<Vec<Cell>> {\n*675|    vec![vec![Cell::default(); cols]; rows]\n 676|}\n 677|\n 678|/// Reallocate a buffer to `rows × cols`, copying the overlapping top-left region.\n*679|fn resized_buffer(old: &[Vec<Cell>], rows: usize, cols: usize) -> Vec<Vec<Cell>> {\n 680|    let mut buf = blank_buffer(rows, cols);\n 681|    for (r, line) in old.iter().take(rows).enumerate() {\n 682|        for (c, cell) in line.iter().take(cols).enumerate() {\n...\n 700|impl Perform for GridState {\n*701|    fn print(&mut self, c: char) {\n 702|        self.put_char(c);\n 703|    }\n 704|\n*705|    fn execute(&mut self, byte: u8) {\n 706|        match byte {\n 707|            0x08 => {\n 708|                // BS\n...\n 733|\n*734|    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\n 735|        let private = intermediates.first() == Some(&b'?');\n 736|        match action {\n 737|            'A' => {\n...\n 821|\n*822|    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {\n 823|        // Ignore charset-designation escapes (intermediates present, e.g. ESC ( B).\n 824|        if !intermediates.is_empty() {\n 825|            return;\n...\n 976|    #[test]\n*977|    fn resize_preserves_title() {\n 978|        let mut g = ScreenGrid::new(24, 80);\n 979|        g.advance(b\"\\x1b]2;sticky\\x07\");\n 980|        g.resize(10, 40);\n...\n 987|    #[test]\n*988|    fn resize_preserves_content() {\n 989|        let mut g = ScreenGrid::new(24, 80);\n 990|        g.advance(b\"keep-me\");\n 991|        g.resize(10, 40);\n...\n 998|    #[test]\n*999|    fn erase_display_clears_screen() {\n 1000|        let out = repaint(&[b\"garbage\\x1b[2J\\x1b[Hclean\"], 24, 80);\n 1001|        assert!(!out.contains(\"garbage\"), \"cleared text is gone: {out:?}\");\n 1002|        assert!(out.contains(\"clean\"), \"post-clear text remains\");"}],"details":{"scopePath":"crates/spt-term/src/screen.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\screen.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":22,"fileCount":1,"files":["crates/spt-term/src/screen.rs"],"fileMatches":[{"path":"crates/spt-term/src/screen.rs","count":22}],"truncated":false,"displayContent":"  168│    /// no latency to controller/viewer delivery (ADR-0031, REQ-HAZARD-INJECT-CONTROL-COEXIST).\n *169│    pub fn advance(&mut self, bytes: &[u8]) {\n  170│        for &b in bytes {\n  171│            // Disjoint field borrows: the parser and the state it drives are\n  172│            // separate fields of `self`, so this compiles without interior mutability.\n     │...\n  178│    /// top-left overlap and clamping the cursor.\n *179│    pub fn resize(&mut self, rows: u16, cols: u16) {\n  180│        self.state.resize(rows, cols);\n  181│    }\n  182│\n     │...\n  187│    /// raw-ring initial batch at `become_controller` / `add_viewer`.\n *188│    pub fn render_repaint(&self) -> Vec<u8> {\n  189│        self.state.render_repaint()\n  190│    }\n  191│}\n     │...\n  256│    /// preserved (so a status-bar erase keeps its colour).\n *257│    fn erase_pen(&self) -> Pen {\n  258│        Pen {\n  259│            bg: self.pen.bg,\n  260│            ..Pen::default()\n     │...\n  298│\n *299│    fn scroll_up(&mut self, n: usize) {\n  300│        let blank = self.blank();\n  301│        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n  302│        let cols = self.cols;\n     │...\n  311│\n *312│    fn scroll_down(&mut self, n: usize) {\n  313│        let blank = self.blank();\n  314│        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n  315│        let cols = self.cols;\n     │...\n  346│\n *347│    fn erase_display(&mut self, mode: u16) {\n  348│        let blank = self.blank();\n  349│        let (row, col, rows, cols) = (self.row, self.col, self.rows, self.cols);\n  350│        let buf = self.buf_mut();\n     │...\n  384│\n *385│    fn erase_line(&mut self, mode: u16) {\n  386│        let blank = self.blank();\n  387│        let (row, col, cols) = (self.row, self.col, self.cols);\n  388│        let buf = self.buf_mut();\n     │...\n  407│\n *408│    fn erase_chars(&mut self, n: usize) {\n  409│        let blank = self.blank();\n  410│        let (row, col, cols) = (self.row, self.col, self.cols);\n  411│        let buf = self.buf_mut();\n     │...\n  430│\n *431│    fn delete_chars(&mut self, n: usize) {\n  432│        let blank = self.blank();\n  433│        let (row, col, cols) = (self.row, self.col, self.cols);\n  434│        let buf = self.buf_mut();\n     │...\n  454│\n *455│    fn delete_lines(&mut self, n: usize) {\n  456│        if self.row < self.scroll_top || self.row > self.scroll_bottom {\n  457│            return;\n  458│        }\n     │...\n  556│\n *557│    fn resize(&mut self, rows: u16, cols: u16) {\n  558│        let rows = rows.max(1) as usize;\n  559│        let cols = cols.max(1) as usize;\n  560│        self.main = resized_buffer(&self.main, rows, cols);\n     │...\n  576│\n *577│    fn render_repaint(&self) -> Vec<u8> {\n  578│        let mut out = String::new();\n  579│        // Put the client on the matching buffer so its own scrollback is never\n  580│        // touched: 1049h saves+switches+clears the alt screen; 1049l returns to main.\n     │...\n  674│fn blank_buffer(rows: usize, cols: usize) -> Vec<Vec<Cell>> {\n *675│    vec![vec![Cell::default(); cols]; rows]\n  676│}\n  677│\n  678│/// Reallocate a buffer to `rows × cols`, copying the overlapping top-left region.\n *679│fn resized_buffer(old: &[Vec<Cell>], rows: usize, cols: usize) -> Vec<Vec<Cell>> {\n  680│    let mut buf = blank_buffer(rows, cols);\n  681│    for (r, line) in old.iter().take(rows).enumerate() {\n  682│        for (c, cell) in line.iter().take(cols).enumerate() {\n     │...\n  700│impl Perform for GridState {\n *701│    fn print(&mut self, c: char) {\n  702│        self.put_char(c);\n  703│    }\n  704│\n *705│    fn execute(&mut self, byte: u8) {\n  706│        match byte {\n  707│            0x08 => {\n  708│                // BS\n     │...\n  733│\n *734│    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\n  735│        let private = intermediates.first() == Some(&b'?');\n  736│        match action {\n  737│            'A' => {\n     │...\n  821│\n *822│    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {\n  823│        // Ignore charset-designation escapes (intermediates present, e.g. ESC ( B).\n  824│        if !intermediates.is_empty() {\n  825│            return;\n     │...\n  976│    #[test]\n *977│    fn resize_preserves_title() {\n  978│        let mut g = ScreenGrid::new(24, 80);\n  979│        g.advance(b\"\\x1b]2;sticky\\x07\");\n  980│        g.resize(10, 40);\n     │...\n  987│    #[test]\n *988│    fn resize_preserves_content() {\n  989│        let mut g = ScreenGrid::new(24, 80);\n  990│        g.advance(b\"keep-me\");\n  991│        g.resize(10, 40);\n     │...\n  998│    #[test]\n *999│    fn erase_display_clears_screen() {\n 1000│        let out = repaint(&[b\"garbage\\x1b[2J\\x1b[Hclean\"], 24, 80);\n 1001│        assert!(!out.contains(\"garbage\"), \"cleared text is gone: {out:?}\");\n 1002│        assert!(out.contains(\"clean\"), \"post-clear text remains\");"},"isError":false,"timestamp":1784365494680}}
{"type":"message","id":"8d2ba5cf","parentId":"f006f490","timestamp":"2026-07-18T09:04:54.686Z","message":{"role":"toolResult","toolCallId":"call_GUe7BEreCuHqfYoFBbz8Vlho|fc_0ce5441d29b9f05d016a5b41b69c50819badd7a99a9f45d0bc","toolName":"grep","content":[{"type":"text","text":" 12|//!\n*13|//! **Detach never kills the session.** The broker owns the PTY; closing our send\n 14|//! side ends the viewport with the session still running (`serve_attach` →\n*15|//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\n 16|//! attach-lifecycle invariant).\n 17|//!\n 18|//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\n...\n 50|    /// The operator hit the detach keybind — end the viewport, leave the session.\n*51|    Detach,\n 52|}\n 53|\n 54|/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\n...\n 276|    // [impl->REQ-RC-IDENTITY]\n*277|    fn restore_bytes() -> Vec<u8> {\n 278|        let mut out = Vec::new();\n 279|        out.extend_from_slice(b\"\\x1b[r\"); // reset scroll region (full screen)\n 280|        out.extend_from_slice(b\"\\x1b[1;1H\"); // home to row 1\n...\n 610|    /// The operator hit `ctrl-b d` — end the viewport.\n*611|    Detach,\n 612|    /// Bytes to forward to the session (empty ⇒ nothing to send).\n 613|    Forward(Vec<u8>),\n 614|    /// Saw the lone `ctrl-b` prefix — armed; nothing to forward yet.\n...\n 619|/// [`parse_stdin_chunk`] (the byte path) so both inputs share IDENTICAL detach\n*620|/// semantics (doyle ruling): Ctrl+B arms; armed + plain `'d'` ⇒ Detach; armed +\n 621|/// Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` then the translated event. The\n 622|/// detach key is PLAIN `'d'` only (a Ctrl+D stays prefix + `0x04`, matching the byte\n 623|/// SM's bare-`DETACH_KEY`). Pure given `armed`, so it unit-tests without a console.\n...\n 632|        if ke.code == KeyCode::Char('d') && ke.modifiers.is_empty() {\n*633|            return KeyAction::Detach;\n 634|        }\n 635|        if is_ctrl_b {\n 636|            return KeyAction::Forward(vec![DETACH_PREFIX]); // literal ctrl-b\n...\n 737|/// console (the events reader), so a redirected/piped stdin (tests) never has\n*738|/// mouse-capture escapes written to its stdout. Stable within a session.\n 739|// [impl->REQ-RC-WIN-PASTE]\n 740|#[cfg(windows)]\n 741|fn windows_mouse_wanted() -> bool {\n...\n 761|/// Enable VT-output on the real console and return the prior mode to restore.\n*762|/// `None` when there is no real console (a piped stdout → `GetConsoleMode` fails)\n 763|/// or the calls error — fail-safe: the byte pump is unchanged, so a redirected\n*764|/// stdout keeps clean bytes for the e2e tests even if the interactive guard passed.\n 765|// [impl->REQ-RC-WIN-VT-OUTPUT]\n 766|#[cfg(windows)]\n 767|fn enable_vt_output() -> Option<u32> {\n...\n 803|/// pump returns.\n*804|struct RawGuard {\n 805|    raw: bool,\n 806|    /// The prior STD_OUTPUT_HANDLE console mode to restore on drop (#12,\n 807|    /// REQ-RC-WIN-VT-OUTPUT) — `Some` only when we enabled VT-output on an\n...\n 811|}\n*812|impl RawGuard {\n 813|    fn enable() -> Self {\n 814|        let raw = crossterm::terminal::enable_raw_mode().is_ok();\n 815|        #[cfg(windows)]\n...\n 819|        // for the right-click paste gesture (REQ-RC-WIN-PASTE). Interactive console\n*820|        // only — a piped stdin keeps a clean stdout for the e2e byte tests.\n 821|        #[cfg(windows)]\n 822|        if windows_mouse_wanted() {\n*823|            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture);\n 824|            // Enable VT-OUTPUT so the remote harness's ANSI escapes render instead\n 825|            // of printing literally on a Win10 conhost console (#12,\n 826|            // REQ-RC-WIN-VT-OUTPUT). Same interactive-console guard as the mouse\n*827|            // capture; a piped stdout fails GetConsoleMode → no-op → clean e2e\n 828|            // bytes. Prior mode captured for restore on drop.\n 829|            prior_out_mode = enable_vt_output();\n 830|        }\n...\n 832|        {\n*833|            RawGuard { raw, prior_out_mode }\n 834|        }\n 835|        #[cfg(not(windows))]\n 836|        {\n*837|            RawGuard { raw }\n 838|        }\n 839|    }\n 840|}\n*841|impl Drop for RawGuard {\n 842|    fn drop(&mut self) {\n 843|        #[cfg(windows)]\n 844|        if windows_mouse_wanted() {\n*845|            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);\n 846|        }\n 847|        // Restore the prior VT-output console mode (#12, REQ-RC-WIN-VT-OUTPUT).\n 848|        #[cfg(windows)]\n...\n 947|/// forward to the session, and whether the detach keybind fired.\n*948|struct DetachParse {\n 949|    forward: Vec<u8>,\n 950|    detach: bool,\n 951|}\n...\n 959|/// viewport is ending).\n*960|fn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {\n 961|    let mut forward = Vec::with_capacity(input.len());\n 962|    for &b in input {\n 963|        if *armed {\n...\n 965|            match b {\n*966|                DETACH_KEY => return DetachParse { forward, detach: true },\n 967|                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b\n 968|                other => {\n 969|                    forward.push(DETACH_PREFIX);\n...\n 978|    }\n*979|    DetachParse {\n 980|        forward,\n 981|        detach: false,\n 982|    }\n...\n 990|/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection\n*991|/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.\n 992|fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n 993|    #[cfg(windows)]\n 994|    {\n...\n 1028|            if parsed.detach {\n*1029|                let _ = tx.send(StdinMsg::Detach);\n 1030|                return;\n 1031|            }\n 1032|        }\n...\n 1079|                    match key_event_step(&mut armed, ke) {\n*1080|                        KeyAction::Detach => {\n*1081|                            let _ = tx.send(StdinMsg::Detach);\n 1082|                            return;\n 1083|                        }\n 1084|                        KeyAction::Forward(bytes) => {\n...\n 1110|pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n*1111|    run_attach_inner(endpoint_id, intent, false)\n 1112|}\n 1113|\n 1114|/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n...\n 1127|) -> Result<(), String> {\n*1128|    run_attach_inner(endpoint_id, intent, true)\n 1129|}\n 1130|\n*1131|fn run_attach_inner(\n 1132|    endpoint_id: &str,\n 1133|    intent: AttachIntent,\n 1134|    session_confirmed: bool,\n...\n 1212|\n*1213|    let _raw = RawGuard::enable();\n 1214|    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n 1215|    // pump and read by the Windows stdin reader to gate scroll forwarding\n 1216|    // (REQ-RC-MOUSE-FORWARD).\n...\n 1240|\n*1241|    let mut stdout = std::io::stdout();\n 1242|    let outcome = attach_viewport(\n 1243|        endpoint_id,\n 1244|        intent,\n...\n 1250|        &mut remote_node,\n*1251|        &mut stdout,\n 1252|    );\n 1253|\n 1254|    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n...\n 1256|    if status.is_some() {\n*1257|        let _ = stdout.write_all(&StatusRow::restore_bytes());\n*1258|        let _ = stdout.flush();\n 1259|    }\n 1260|    // Out of raw mode (guard drops after) before the parting line.\n 1261|    drop(_raw);\n 1262|    match outcome {\n*1263|        Ok(PumpEnd::Exited(code)) => {\n*1264|            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n 1265|            Ok(())\n 1266|        }\n*1267|        Ok(PumpEnd::Detached) => {\n*1268|            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n 1269|            Ok(())\n 1270|        }\n 1271|        Ok(PumpEnd::Displaced(by)) => {\n...\n 1273|            let _ = writeln!(\n*1274|                stdout,\n 1275|                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n 1276|            );\n 1277|            Ok(())\n...\n 1287|                let _ = writeln!(\n*1288|                    stdout,\n 1289|                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n 1290|                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n 1291|                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n...\n 1295|                let _ = writeln!(\n*1296|                    stdout,\n 1297|                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n 1298|                     within {}s — the session may still be running. Try `spt rc \\\n 1299|                     {endpoint_id}` again once the node/daemon is back]\",\n...\n 1305|        // Reconnectable ends never escape the loop (they enter the retry arm and\n*1306|        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n 1307|        // so a future PumpEnd variant still forces a decision here.\n 1308|        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n 1309|            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n...\n 1315|            let _ = writeln!(\n*1316|                stdout,\n 1317|                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\n*1318|                 wedged. Detached. Try `spt endpoint list` for its state, or \\\n 1319|                 `spt endpoint run` to restart it]\"\n 1320|            );\n 1321|            Ok(())\n...\n 1329|                    let _ = writeln!(\n*1330|                        stdout,\n 1331|                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\n 1332|                         have ended, or the registry row is stale]\"\n 1333|                    );\n...\n 1335|                None => {\n*1336|                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\n 1337|                }\n 1338|            }\n 1339|            Ok(())\n...\n 1396|/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n*1397|/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n 1398|/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n 1399|///\n 1400|/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n...\n 1433|    // pipes), so the single-threaded pump interleaves stdin + broker events\n*1434|    // without wedging. The generous io_timeout bounds the setup calls (dial,\n 1435|    // sessions, attach-open ack); the pump itself passes its own short slice.\n 1436|    let mut brain = Brain::cold_start_pump(\n 1437|        &spt_daemon::endpoint::broker_socket_name(),\n...\n 1613|            let remaining = reconnect_remaining_secs(elapsed);\n*1614|            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\n*1615|            let _ = out.flush();\n 1616|            // Operator gives up mid-reconnect: honor the detach keys.\n 1617|            if let Some(rx) = rx {\n 1618|                let mut detached = false;\n 1619|                while let Ok(msg) = rx.try_recv() {\n*1620|                    if matches!(msg, StdinMsg::Detach) {\n 1621|                        detached = true;\n 1622|                    }\n 1623|                }\n...\n 1642|            }\n*1643|            Some(Err(())) => return Ok(PumpEnd::Detached),\n 1644|            None => {\n 1645|                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\n 1646|                // daemon (only knowable for a LOCAL target) gets the loud \"session\n...\n 1701|        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n*1702|        PumpEnd::Exited(_)\n*1703|        | PumpEnd::Detached\n 1704|        | PumpEnd::Displaced(_)\n 1705|        | PumpEnd::Stalled\n 1706|        | PumpEnd::NoLiveSession\n...\n 1748|enum PumpEnd {\n*1749|    Exited(Option<i32>),\n*1750|    Detached,\n 1751|    /// A different operator took control (REQ-KICK-1); carries the taker's node.\n 1752|    Displaced(String),\n 1753|    /// The broker stream was severed mid-session (broker bounce / hang-up — e.g. a\n...\n 1759|    /// conn dropped (node bounce, network cut), NOT an operator detach (that\n*1760|    /// returns via the stdin `Detach` path directly) and NOT a session end (an\n*1761|    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\n 1762|    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\n 1763|    /// re-drives. [impl->REQ-RC-RECONNECT]\n 1764|    Severed,\n...\n 1784|    /// the D6 stale-row case: the registry routed us to a node that no longer has\n*1785|    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\n 1786|    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\n 1787|    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n 1788|    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n...\n 1809|enum ReadDisposition {\n*1810|    /// Transient poll-slice timeout — loop and poll again.\n 1811|    Retry,\n 1812|    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n 1813|    /// gracefully.\n...\n 1818|\n*1819|/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n 1820|/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n 1821|/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n 1822|/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n...\n 1828|    match kind {\n*1829|        WouldBlock | TimedOut => ReadDisposition::Retry,\n 1830|        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n 1831|            ReadDisposition::BrokerGone\n 1832|        }\n...\n 1837|/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n*1838|/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n 1839|#[allow(clippy::too_many_arguments)]\n 1840|fn pump(\n 1841|    brain: &mut Brain,\n...\n 1845|    decoder: &mut AttachDecoder,\n*1846|    stdout: &mut impl std::io::Write,\n 1847|    controller: bool,\n 1848|    mouse_mode: &MouseMode,\n 1849|    mut status: Option<&mut StatusRow>,\n...\n 1863|    if let Some(status) = status.as_deref_mut() {\n*1864|        stdout\n*1865|            .write_all(&status.assert_bytes())\n 1866|            .map_err(|e| format!(\"status assert: {e}\"))?;\n*1867|        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n 1868|    }\n*1869|    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n 1870|    // (the session outlives the viewport). Best-effort: an already-finished or\n 1871|    // exited stream send is harmless to attempt.\n 1872|    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n 1873|        let _ = brain.net_stream_send(stream_id, &[], None, true);\n*1874|        PumpEnd::Detached\n 1875|    }\n 1876|    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n 1877|    // drop any `seq` already rendered (the operator render cursor, matching the\n...\n 1912|                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n*1913|                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n*1914|                                let _ = stdout.flush();\n 1915|                            }\n 1916|                        }\n 1917|                        None => {\n...\n 1932|                    }\n*1933|                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n 1934|                    Err(mpsc::TryRecvError::Empty) => break,\n 1935|                    Err(mpsc::TryRecvError::Disconnected) => {\n 1936|                        return Ok(detach(brain, stream_id))\n...\n 1945|            Err(e) => match classify_read_err(e.kind()) {\n*1946|                // A poll-slice timeout: no event this slice. Before looping, the\n 1947|                // first-event backstop — if NOTHING has ever arrived past the\n 1948|                // generous grace, the attach is dead/wedged (path b).\n 1949|                ReadDisposition::Retry => {\n...\n 1976|                            mouse_scanner.feed(mouse_mode, &bytes);\n*1977|                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n 1978|                            // Re-assert the reserved row if the harness output just\n 1979|                            // destroyed our scroll region — alt-screen enter or a\n 1980|                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n...\n 1987|                                if reassert_scanner.feed(&bytes) {\n*1988|                                    stdout\n*1989|                                        .write_all(&status.assert_bytes())\n 1990|                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n 1991|                                }\n 1992|                            }\n*1993|                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n 1994|                        }\n*1995|                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n 1996|                        AttachRecord::Displaced { by } => {\n 1997|                            return Ok(PumpEnd::Displaced(by));\n 1998|                        }\n...\n 2007|                                    let _ = write!(\n*2008|                                        stdout,\n 2009|                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n 2010|                                    );\n*2011|                                    let _ = stdout.flush();\n 2012|                                }\n 2013|                                _ => {}\n 2014|                            }\n...\n 2113|        }\n*2114|        fn flush(&mut self) -> std::io::Result<()> {\n 2115|            Ok(())\n 2116|        }\n 2117|    }\n...\n 2146|    }\n*2147|    impl Drop for SeedGuard {\n 2148|        fn drop(&mut self) {\n 2149|            if !self.stopped {\n 2150|                self.stopped = true;\n...\n 2159|    // reconnect banner, then post-bounce output from the re-established serve,\n*2160|    // and the viewport still ends HONESTLY (session exit → Exited). Everything\n 2161|    // is in-process against an isolated home: the broker + seed sockets are\n 2162|    // home-tagged (no live-box collision), and the seed channel stays UP through\n 2163|    // the bounce so establish_attach's CONNECT-ONLY is_running() probe stays\n...\n 2363|\n*2364|        // Honest finish: the ticker exits on the flag → Exit record → the\n*2365|        // viewport returns Exited (a FINAL end — never re-driven).\n 2366|        std::fs::write(&flag, b\"done\").expect(\"write exit flag\");\n 2367|        host2.kill_session().expect(\"kill the ticker session\");\n 2368|        let outcome = viewport.join().expect(\"viewport thread joins\");\n...\n 2373|        match outcome {\n*2374|            Ok(PumpEnd::Exited(_)) => {}\n 2375|            other => panic!(\"expected the session exit to END the viewport, got {other:?}\"),\n 2376|        }\n 2377|    }\n...\n 2420|        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\n*2421|        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n*2422|        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\n 2423|        assert_eq!(\n 2424|            reconnect_class(&PumpEnd::Displaced(\"node\".into())),\n 2425|            EndClass::Final\n...\n 2527|    // says rc won't auto-start it, points at `daemon start`); a generic give-up keeps\n*2528|    // the didn't-reconnect copy. Renders the exact bytes run_attach_inner writes so\n 2529|    // the branch is proven without standing up a broker.\n 2530|    #[test]\n 2531|    fn give_up_copy_names_daemon_down_vs_generic() {\n...\n 2628|    // classified BrokerGone → graceful surface, NOT a fatal RC_FAIL crash; a\n*2629|    // poll-slice timeout retries; any other error stays fatal.\n 2630|    #[test]\n 2631|    fn classify_read_err_eof_is_graceful_not_fatal() {\n 2632|        use std::io::ErrorKind::*;\n...\n 2638|        assert_eq!(classify_read_err(ConnectionAborted), ReadDisposition::BrokerGone);\n*2639|        // Poll-slice timeouts retry (no event this slice).\n 2640|        assert_eq!(classify_read_err(WouldBlock), ReadDisposition::Retry);\n*2641|        assert_eq!(classify_read_err(TimedOut), ReadDisposition::Retry);\n 2642|        // A genuine unexpected failure still propagates.\n 2643|        assert_eq!(classify_read_err(InvalidData), ReadDisposition::Fatal);\n 2644|        assert_eq!(classify_read_err(PermissionDenied), ReadDisposition::Fatal);\n...\n 2840|\n*2841|        // armed + plain 'd' → Detach.\n 2842|        match key_event_step(&mut armed, ke(KeyCode::Char('d'), none)) {\n*2843|            KeyAction::Detach => {}\n*2844|            other => panic!(\"armed + 'd' should Detach, got {:?}\", action_name(&other)),\n 2845|        }\n*2846|        assert!(!armed, \"Detach disarms\");\n 2847|\n 2848|        // armed + Ctrl+B → Forward(literal 0x02), disarms.\n 2849|        armed = true;\n...\n 2863|\n*2864|        // armed + Ctrl+D → NOT Detach: Forward(0x02 then 0x04). Detach key is PLAIN 'd'.\n 2865|        armed = true;\n 2866|        match key_event_step(&mut armed, ke(KeyCode::Char('d'), ctrl)) {\n 2867|            KeyAction::Forward(v) => assert_eq!(\n...\n 2871|            ),\n*2872|            other => panic!(\"armed + Ctrl+D must NOT Detach, got {:?}\", action_name(&other)),\n 2873|        }\n 2874|        assert!(!armed);\n 2875|\n...\n 2888|        match a {\n*2889|            KeyAction::Detach => \"Detach\",\n 2890|            KeyAction::Forward(_) => \"Forward\",\n 2891|            KeyAction::Swallow => \"Swallow\",\n 2892|        }\n...\n 3119|    fn status_restore_exact_bytes() {\n*3120|        assert_eq!(StatusRow::restore_bytes(), b\"\\x1b[r\\x1b[1;1H\\x1b[2K\".to_vec());\n 3121|    }\n 3122|\n 3123|    // [unit->REQ-RC-IDENTITY] update_size re-points the tracked terminal size (the\n...\n 3183|    // re-assert scanner (an alt-screen enter) it writes `assert_bytes()` (margin +\n*3184|    // paint — the region was destroyed); (c) on exit it writes `restore_bytes()`.\n 3185|    // Asserting the captured byte stream proves\n 3186|    // the reserved-row contract holds through the pump's status hooks WITHOUT a live\n 3187|    // broker (rc's pump needs a Brain + PTY peer that an in-process unit can't\n...\n 3201|        // (a) pump START: reserve + paint.\n*3202|        sink.write_all(&status.assert_bytes()).unwrap();\n 3203|        let assert_len = sink.len();\n 3204|        assert!(sink.starts_with(b\"\\x1b[2;24r\"), \"start reserves the row\");\n 3205|        assert!(\n...\n 3212|        if scanner.feed(plain) {\n*3213|            sink.write_all(&status.assert_bytes()).unwrap();\n 3214|        }\n 3215|        assert_eq!(sink.len(), assert_len, \"plain output emits no status re-assert\");\n 3216|\n...\n 3224|            fired = true;\n*3225|            sink.write_all(&status.assert_bytes()).unwrap();\n 3226|        }\n 3227|        assert!(fired, \"the alt-screen enter trips the re-assert scanner\");\n 3228|        assert!(sink.len() > before, \"a re-assert was appended after the trigger\");\n...\n 3234|        // (c) pump EXIT: restore (reset region + clear row 1).\n*3235|        sink.write_all(&StatusRow::restore_bytes()).unwrap();\n 3236|        assert!(\n 3237|            sink.ends_with(b\"\\x1b[r\\x1b[1;1H\\x1b[2K\"),\n 3238|            \"exit restores the full scroll region and clears the reserved row\""}],"details":{"scopePath":"crates/spt/src/rc.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":106,"fileCount":1,"files":["crates/spt/src/rc.rs"],"fileMatches":[{"path":"crates/spt/src/rc.rs","count":106}],"truncated":false,"displayContent":"   12│//!\n  *13│//! **Detach never kills the session.** The broker owns the PTY; closing our send\n   14│//! side ends the viewport with the session still running (`serve_attach` →\n  *15│//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS\n   16│//! attach-lifecycle invariant).\n   17│//!\n   18│//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread\n     │...\n   50│    /// The operator hit the detach keybind — end the viewport, leave the session.\n  *51│    Detach,\n   52│}\n   53│\n   54│/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET\n     │...\n  276│    // [impl->REQ-RC-IDENTITY]\n *277│    fn restore_bytes() -> Vec<u8> {\n  278│        let mut out = Vec::new();\n  279│        out.extend_from_slice(b\"\\x1b[r\"); // reset scroll region (full screen)\n  280│        out.extend_from_slice(b\"\\x1b[1;1H\"); // home to row 1\n     │...\n  610│    /// The operator hit `ctrl-b d` — end the viewport.\n *611│    Detach,\n  612│    /// Bytes to forward to the session (empty ⇒ nothing to send).\n  613│    Forward(Vec<u8>),\n  614│    /// Saw the lone `ctrl-b` prefix — armed; nothing to forward yet.\n     │...\n  619│/// [`parse_stdin_chunk`] (the byte path) so both inputs share IDENTICAL detach\n *620│/// semantics (doyle ruling): Ctrl+B arms; armed + plain `'d'` ⇒ Detach; armed +\n  621│/// Ctrl+B ⇒ literal `0x02`; armed + other ⇒ `0x02` then the translated event. The\n  622│/// detach key is PLAIN `'d'` only (a Ctrl+D stays prefix + `0x04`, matching the byte\n  623│/// SM's bare-`DETACH_KEY`). Pure given `armed`, so it unit-tests without a console.\n     │...\n  632│        if ke.code == KeyCode::Char('d') && ke.modifiers.is_empty() {\n *633│            return KeyAction::Detach;\n  634│        }\n  635│        if is_ctrl_b {\n  636│            return KeyAction::Forward(vec![DETACH_PREFIX]); // literal ctrl-b\n     │...\n  737│/// console (the events reader), so a redirected/piped stdin (tests) never has\n *738│/// mouse-capture escapes written to its stdout. Stable within a session.\n  739│// [impl->REQ-RC-WIN-PASTE]\n  740│#[cfg(windows)]\n  741│fn windows_mouse_wanted() -> bool {\n     │...\n  761│/// Enable VT-output on the real console and return the prior mode to restore.\n *762│/// `None` when there is no real console (a piped stdout → `GetConsoleMode` fails)\n  763│/// or the calls error — fail-safe: the byte pump is unchanged, so a redirected\n *764│/// stdout keeps clean bytes for the e2e tests even if the interactive guard passed.\n  765│// [impl->REQ-RC-WIN-VT-OUTPUT]\n  766│#[cfg(windows)]\n  767│fn enable_vt_output() -> Option<u32> {\n     │...\n  803│/// pump returns.\n *804│struct RawGuard {\n  805│    raw: bool,\n  806│    /// The prior STD_OUTPUT_HANDLE console mode to restore on drop (#12,\n  807│    /// REQ-RC-WIN-VT-OUTPUT) — `Some` only when we enabled VT-output on an\n     │...\n  811│}\n *812│impl RawGuard {\n  813│    fn enable() -> Self {\n  814│        let raw = crossterm::terminal::enable_raw_mode().is_ok();\n  815│        #[cfg(windows)]\n     │...\n  819│        // for the right-click paste gesture (REQ-RC-WIN-PASTE). Interactive console\n *820│        // only — a piped stdin keeps a clean stdout for the e2e byte tests.\n  821│        #[cfg(windows)]\n  822│        if windows_mouse_wanted() {\n *823│            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture);\n  824│            // Enable VT-OUTPUT so the remote harness's ANSI escapes render instead\n  825│            // of printing literally on a Win10 conhost console (#12,\n  826│            // REQ-RC-WIN-VT-OUTPUT). Same interactive-console guard as the mouse\n *827│            // capture; a piped stdout fails GetConsoleMode → no-op → clean e2e\n  828│            // bytes. Prior mode captured for restore on drop.\n  829│            prior_out_mode = enable_vt_output();\n  830│        }\n     │...\n  832│        {\n *833│            RawGuard { raw, prior_out_mode }\n  834│        }\n  835│        #[cfg(not(windows))]\n  836│        {\n *837│            RawGuard { raw }\n  838│        }\n  839│    }\n  840│}\n *841│impl Drop for RawGuard {\n  842│    fn drop(&mut self) {\n  843│        #[cfg(windows)]\n  844│        if windows_mouse_wanted() {\n *845│            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);\n  846│        }\n  847│        // Restore the prior VT-output console mode (#12, REQ-RC-WIN-VT-OUTPUT).\n  848│        #[cfg(windows)]\n     │...\n  947│/// forward to the session, and whether the detach keybind fired.\n *948│struct DetachParse {\n  949│    forward: Vec<u8>,\n  950│    detach: bool,\n  951│}\n     │...\n  959│/// viewport is ending).\n *960│fn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {\n  961│    let mut forward = Vec::with_capacity(input.len());\n  962│    for &b in input {\n  963│        if *armed {\n     │...\n  965│            match b {\n *966│                DETACH_KEY => return DetachParse { forward, detach: true },\n  967│                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b\n  968│                other => {\n  969│                    forward.push(DETACH_PREFIX);\n     │...\n  978│    }\n *979│    DetachParse {\n  980│        forward,\n  981│        detach: false,\n  982│    }\n     │...\n  990│/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection\n *991│/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.\n  992│fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n  993│    #[cfg(windows)]\n  994│    {\n     │...\n 1028│            if parsed.detach {\n*1029│                let _ = tx.send(StdinMsg::Detach);\n 1030│                return;\n 1031│            }\n 1032│        }\n     │...\n 1079│                    match key_event_step(&mut armed, ke) {\n*1080│                        KeyAction::Detach => {\n*1081│                            let _ = tx.send(StdinMsg::Detach);\n 1082│                            return;\n 1083│                        }\n 1084│                        KeyAction::Forward(bytes) => {\n     │...\n 1110│pub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n*1111│    run_attach_inner(endpoint_id, intent, false)\n 1112│}\n 1113│\n 1114│/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n     │...\n 1127│) -> Result<(), String> {\n*1128│    run_attach_inner(endpoint_id, intent, true)\n 1129│}\n 1130│\n*1131│fn run_attach_inner(\n 1132│    endpoint_id: &str,\n 1133│    intent: AttachIntent,\n 1134│    session_confirmed: bool,\n     │...\n 1212│\n*1213│    let _raw = RawGuard::enable();\n 1214│    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n 1215│    // pump and read by the Windows stdin reader to gate scroll forwarding\n 1216│    // (REQ-RC-MOUSE-FORWARD).\n     │...\n 1240│\n*1241│    let mut stdout = std::io::stdout();\n 1242│    let outcome = attach_viewport(\n 1243│        endpoint_id,\n 1244│        intent,\n     │...\n 1250│        &mut remote_node,\n*1251│        &mut stdout,\n 1252│    );\n 1253│\n 1254│    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n     │...\n 1256│    if status.is_some() {\n*1257│        let _ = stdout.write_all(&StatusRow::restore_bytes());\n*1258│        let _ = stdout.flush();\n 1259│    }\n 1260│    // Out of raw mode (guard drops after) before the parting line.\n 1261│    drop(_raw);\n 1262│    match outcome {\n*1263│        Ok(PumpEnd::Exited(code)) => {\n*1264│            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n 1265│            Ok(())\n 1266│        }\n*1267│        Ok(PumpEnd::Detached) => {\n*1268│            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n 1269│            Ok(())\n 1270│        }\n 1271│        Ok(PumpEnd::Displaced(by)) => {\n     │...\n 1273│            let _ = writeln!(\n*1274│                stdout,\n 1275│                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n 1276│            );\n 1277│            Ok(())\n     │...\n 1287│                let _ = writeln!(\n*1288│                    stdout,\n 1289│                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n 1290│                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n 1291│                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n     │...\n 1295│                let _ = writeln!(\n*1296│                    stdout,\n 1297│                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n 1298│                     within {}s — the session may still be running. Try `spt rc \\\n 1299│                     {endpoint_id}` again once the node/daemon is back]\",\n     │...\n 1305│        // Reconnectable ends never escape the loop (they enter the retry arm and\n*1306│        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n 1307│        // so a future PumpEnd variant still forces a decision here.\n 1308│        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n 1309│            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n     │...\n 1315│            let _ = writeln!(\n*1316│                stdout,\n 1317│                \"\\r\\n[no output from '{endpoint_id}' — the session may be dead or \\\n*1318│                 wedged. Detached. Try `spt endpoint list` for its state, or \\\n 1319│                 `spt endpoint run` to restart it]\"\n 1320│            );\n 1321│            Ok(())\n     │...\n 1329│                    let _ = writeln!(\n*1330│                        stdout,\n 1331│                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\n 1332│                         have ended, or the registry row is stale]\"\n 1333│                    );\n     │...\n 1335│                None => {\n*1336│                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\n 1337│                }\n 1338│            }\n 1339│            Ok(())\n     │...\n 1396│/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n*1397│/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n 1398│/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n 1399│///\n 1400│/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n     │...\n 1433│    // pipes), so the single-threaded pump interleaves stdin + broker events\n*1434│    // without wedging. The generous io_timeout bounds the setup calls (dial,\n 1435│    // sessions, attach-open ack); the pump itself passes its own short slice.\n 1436│    let mut brain = Brain::cold_start_pump(\n 1437│        &spt_daemon::endpoint::broker_socket_name(),\n     │...\n 1613│            let remaining = reconnect_remaining_secs(elapsed);\n*1614│            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\n*1615│            let _ = out.flush();\n 1616│            // Operator gives up mid-reconnect: honor the detach keys.\n 1617│            if let Some(rx) = rx {\n 1618│                let mut detached = false;\n 1619│                while let Ok(msg) = rx.try_recv() {\n*1620│                    if matches!(msg, StdinMsg::Detach) {\n 1621│                        detached = true;\n 1622│                    }\n 1623│                }\n     │...\n 1642│            }\n*1643│            Some(Err(())) => return Ok(PumpEnd::Detached),\n 1644│            None => {\n 1645│                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\n 1646│                // daemon (only knowable for a LOCAL target) gets the loud \"session\n     │...\n 1701│        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n*1702│        PumpEnd::Exited(_)\n*1703│        | PumpEnd::Detached\n 1704│        | PumpEnd::Displaced(_)\n 1705│        | PumpEnd::Stalled\n 1706│        | PumpEnd::NoLiveSession\n     │...\n 1748│enum PumpEnd {\n*1749│    Exited(Option<i32>),\n*1750│    Detached,\n 1751│    /// A different operator took control (REQ-KICK-1); carries the taker's node.\n 1752│    Displaced(String),\n 1753│    /// The broker stream was severed mid-session (broker bounce / hang-up — e.g. a\n     │...\n 1759│    /// conn dropped (node bounce, network cut), NOT an operator detach (that\n*1760│    /// returns via the stdin `Detach` path directly) and NOT a session end (an\n*1761│    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\n 1762│    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\n 1763│    /// re-drives. [impl->REQ-RC-RECONNECT]\n 1764│    Severed,\n     │...\n 1784│    /// the D6 stale-row case: the registry routed us to a node that no longer has\n*1785│    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\n 1786│    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\n 1787│    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n 1788│    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n     │...\n 1809│enum ReadDisposition {\n*1810│    /// Transient poll-slice timeout — loop and poll again.\n 1811│    Retry,\n 1812│    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n 1813│    /// gracefully.\n     │...\n 1818│\n*1819│/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n 1820│/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n 1821│/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n 1822│/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n     │...\n 1828│    match kind {\n*1829│        WouldBlock | TimedOut => ReadDisposition::Retry,\n 1830│        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n 1831│            ReadDisposition::BrokerGone\n 1832│        }\n     │...\n 1837│/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n*1838│/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n 1839│#[allow(clippy::too_many_arguments)]\n 1840│fn pump(\n 1841│    brain: &mut Brain,\n     │...\n 1845│    decoder: &mut AttachDecoder,\n*1846│    stdout: &mut impl std::io::Write,\n 1847│    controller: bool,\n 1848│    mouse_mode: &MouseMode,\n 1849│    mut status: Option<&mut StatusRow>,\n     │...\n 1863│    if let Some(status) = status.as_deref_mut() {\n*1864│        stdout\n*1865│            .write_all(&status.assert_bytes())\n 1866│            .map_err(|e| format!(\"status assert: {e}\"))?;\n*1867│        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n 1868│    }\n*1869│    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n 1870│    // (the session outlives the viewport). Best-effort: an already-finished or\n 1871│    // exited stream send is harmless to attempt.\n 1872│    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n 1873│        let _ = brain.net_stream_send(stream_id, &[], None, true);\n*1874│        PumpEnd::Detached\n 1875│    }\n 1876│    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n 1877│    // drop any `seq` already rendered (the operator render cursor, matching the\n     │...\n 1912│                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n*1913│                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n*1914│                                let _ = stdout.flush();\n 1915│                            }\n 1916│                        }\n 1917│                        None => {\n     │...\n 1932│                    }\n*1933│                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n 1934│                    Err(mpsc::TryRecvError::Empty) => break,\n 1935│                    Err(mpsc::TryRecvError::Disconnected) => {\n 1936│                        return Ok(detach(brain, stream_id))\n     │...\n 1945│            Err(e) => match classify_read_err(e.kind()) {\n*1946│                // A poll-slice timeout: no event this slice. Before looping, the\n 1947│                // first-event backstop — if NOTHING has ever arrived past the\n 1948│                // generous grace, the attach is dead/wedged (path b).\n 1949│                ReadDisposition::Retry => {\n     │...\n 1976│                            mouse_scanner.feed(mouse_mode, &bytes);\n*1977│                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n 1978│                            // Re-assert the reserved row if the harness output just\n 1979│                            // destroyed our scroll region — alt-screen enter or a\n 1980│                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n     │...\n 1987│                                if reassert_scanner.feed(&bytes) {\n*1988│                                    stdout\n*1989│                                        .write_all(&status.assert_bytes())\n 1990│                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n 1991│                                }\n 1992│                            }\n*1993│                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n 1994│                        }\n*1995│                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n 1996│                        AttachRecord::Displaced { by } => {\n 1997│                            return Ok(PumpEnd::Displaced(by));\n 1998│                        }\n     │...\n 2007│                                    let _ = write!(\n*2008│                                        stdout,\n 2009│                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n 2010│                                    );\n*2011│                                    let _ = stdout.flush();\n 2012│                                }\n 2013│                                _ => {}\n 2014│                            }\n     │...\n 2113│        }\n*2114│        fn flush(&mut self) -> std::io::Result<()> {\n 2115│            Ok(())\n 2116│        }\n 2117│    }\n     │...\n 2146│    }\n*2147│    impl Drop for SeedGuard {\n 2148│        fn drop(&mut self) {\n 2149│            if !self.stopped {\n 2150│                self.stopped = true;\n     │...\n 2159│    // reconnect banner, then post-bounce output from the re-established serve,\n*2160│    // and the viewport still ends HONESTLY (session exit → Exited). Everything\n 2161│    // is in-process against an isolated home: the broker + seed sockets are\n 2162│    // home-tagged (no live-box collision), and the seed channel stays UP through\n 2163│    // the bounce so establish_attach's CONNECT-ONLY is_running() probe stays\n     │...\n 2363│\n*2364│        // Honest finish: the ticker exits on the flag → Exit record → the\n*2365│        // viewport returns Exited (a FINAL end — never re-driven).\n 2366│        std::fs::write(&flag, b\"done\").expect(\"write exit flag\");\n 2367│        host2.kill_session().expect(\"kill the ticker session\");\n 2368│        let outcome = viewport.join().expect(\"viewport thread joins\");\n     │...\n 2373│        match outcome {\n*2374│            Ok(PumpEnd::Exited(_)) => {}\n 2375│            other => panic!(\"expected the session exit to END the viewport, got {other:?}\"),\n 2376│        }\n 2377│    }\n     │...\n 2420│        assert_eq!(reconnect_class(&PumpEnd::Severed), EndClass::Reconnect);\n*2421│        assert_eq!(reconnect_class(&PumpEnd::Exited(Some(0))), EndClass::Final);\n*2422│        assert_eq!(reconnect_class(&PumpEnd::Detached), EndClass::Final);\n 2423│        assert_eq!(\n 2424│            reconnect_class(&PumpEnd::Displaced(\"node\".into())),\n 2425│            EndClass::Final\n     │...\n 2527│    // says rc won't auto-start it, points at `daemon start`); a generic give-up keeps\n*2528│    // the didn't-reconnect copy. Renders the exact bytes run_attach_inner writes so\n 2529│    // the branch is proven without standing up a broker.\n 2530│    #[test]\n 2531│    fn give_up_copy_names_daemon_down_vs_generic() {\n     │...\n 2628│    // classified BrokerGone → graceful surface, NOT a fatal RC_FAIL crash; a\n*2629│    // poll-slice timeout retries; any other error stays fatal.\n 2630│    #[test]\n 2631│    fn classify_read_err_eof_is_graceful_not_fatal() {\n 2632│        use std::io::ErrorKind::*;\n     │...\n 2638│        assert_eq!(classify_read_err(ConnectionAborted), ReadDisposition::BrokerGone);\n*2639│        // Poll-slice timeouts retry (no event this slice).\n 2640│        assert_eq!(classify_read_err(WouldBlock), ReadDisposition::Retry);\n*2641│        assert_eq!(classify_read_err(TimedOut), ReadDisposition::Retry);\n 2642│        // A genuine unexpected failure still propagates.\n 2643│        assert_eq!(classify_read_err(InvalidData), ReadDisposition::Fatal);\n 2644│        assert_eq!(classify_read_err(PermissionDenied), ReadDisposition::Fatal);\n     │...\n 2840│\n*2841│        // armed + plain 'd' → Detach.\n 2842│        match key_event_step(&mut armed, ke(KeyCode::Char('d'), none)) {\n*2843│            KeyAction::Detach => {}\n*2844│            other => panic!(\"armed + 'd' should Detach, got {:?}\", action_name(&other)),\n 2845│        }\n*2846│        assert!(!armed, \"Detach disarms\");\n 2847│\n 2848│        // armed + Ctrl+B → Forward(literal 0x02), disarms.\n 2849│        armed = true;\n     │...\n 2863│\n*2864│        // armed + Ctrl+D → NOT Detach: Forward(0x02 then 0x04). Detach key is PLAIN 'd'.\n 2865│        armed = true;\n 2866│        match key_event_step(&mut armed, ke(KeyCode::Char('d'), ctrl)) {\n 2867│            KeyAction::Forward(v) => assert_eq!(\n     │...\n 2871│            ),\n*2872│            other => panic!(\"armed + Ctrl+D must NOT Detach, got {:?}\", action_name(&other)),\n 2873│        }\n 2874│        assert!(!armed);\n 2875│\n     │...\n 2888│        match a {\n*2889│            KeyAction::Detach => \"Detach\",\n 2890│            KeyAction::Forward(_) => \"Forward\",\n 2891│            KeyAction::Swallow => \"Swallow\",\n 2892│        }\n     │...\n 3119│    fn status_restore_exact_bytes() {\n*3120│        assert_eq!(StatusRow::restore_bytes(), b\"\\x1b[r\\x1b[1;1H\\x1b[2K\".to_vec());\n 3121│    }\n 3122│\n 3123│    // [unit->REQ-RC-IDENTITY] update_size re-points the tracked terminal size (the\n     │...\n 3183│    // re-assert scanner (an alt-screen enter) it writes `assert_bytes()` (margin +\n*3184│    // paint — the region was destroyed); (c) on exit it writes `restore_bytes()`.\n 3185│    // Asserting the captured byte stream proves\n 3186│    // the reserved-row contract holds through the pump's status hooks WITHOUT a live\n 3187│    // broker (rc's pump needs a Brain + PTY peer that an in-process unit can't\n     │...\n 3201│        // (a) pump START: reserve + paint.\n*3202│        sink.write_all(&status.assert_bytes()).unwrap();\n 3203│        let assert_len = sink.len();\n 3204│        assert!(sink.starts_with(b\"\\x1b[2;24r\"), \"start reserves the row\");\n 3205│        assert!(\n     │...\n 3212│        if scanner.feed(plain) {\n*3213│            sink.write_all(&status.assert_bytes()).unwrap();\n 3214│        }\n 3215│        assert_eq!(sink.len(), assert_len, \"plain output emits no status re-assert\");\n 3216│\n     │...\n 3224│            fired = true;\n*3225│            sink.write_all(&status.assert_bytes()).unwrap();\n 3226│        }\n 3227│        assert!(fired, \"the alt-screen enter trips the re-assert scanner\");\n 3228│        assert!(sink.len() > before, \"a re-assert was appended after the trigger\");\n     │...\n 3234│        // (c) pump EXIT: restore (reset region + clear row 1).\n*3235│        sink.write_all(&StatusRow::restore_bytes()).unwrap();\n 3236│        assert!(\n 3237│            sink.ends_with(b\"\\x1b[r\\x1b[1;1H\\x1b[2K\"),\n 3238│            \"exit restores the full scroll region and clears the reserved row\""},"isError":false,"timestamp":1784365494686}}
{"type":"message","id":"21aed983","parentId":"8d2ba5cf","timestamp":"2026-07-18T09:04:54.700Z","message":{"role":"toolResult","toolCallId":"call_xt70Ykjirxpu2XWUXMpLM0wn|fc_0ce5441d29b9f05d016a5b41b69bf8819bb175e03c0b54a982","toolName":"read","content":[{"type":"text","text":"//! Server-side **screen grid** — the clean-room render model behind the clean\n//! repaint on attach (field bug #6 / `REQ-BROKER-SCREEN-GRID`, ADR-0031).\n//!\n//! A cold attach used to stream the entire retained raw-byte ring as its initial\n//! batch. For an alt-screen TUI that ring is a *rendering protocol mid-stream*\n//! (alt-screen enter/exit, absolute cursor moves, repaints, a torn oldest chunk);\n//! replayed raw into a fresh terminal it corrupts scrollback (#6) and leaves stray\n//! cells (#7) / resize debris (#8). The [`ScreenGrid`] interprets the same byte\n//! stream into an authoritative **current screen** and synthesizes ONE clean\n//! [`ScreenGrid::render_repaint`] the client can paint into a fresh terminal.\n//!\n//! **Scope (ratified, ADR-0031):** current screen only — the visible viewport\n//! (cell buffer + cursor + main/alt buffer + the modes a repaint needs). Scrollback\n//! stays the raw ring (history). The grid drives ONLY the attach repaint; live\n//! frames after attach are the unchanged raw bytes.\n//!\n//! **Provenance (ADR-0001 / ADR-0031):** the byte→action layer is the vetted `vte`\n//! parser state machine (the one non-clean-room dep — a universal, spec-fragile VT\n//! machine where a hand-roll would reproduce the exact artifacts #6 kills). The\n//! grid semantics on top ([`GridState`] impl of [`vte::Perform`]) are clean-room.\n\nuse vte::{Params, Parser, Perform};\n\n/// A cell colour: the terminal default, a palette index (0–15 basic/bright +\n/// 16–255 the 256-colour cube), or 24-bit truecolour.\n#[derive(Clone, Copy, PartialEq, Eq, Debug)]\nenum Color {\n    Default,\n    Idx(u8),\n    Rgb(u8, u8, u8),\n}\n\n/// The graphic rendition (SGR) state of a cell — foreground/background plus the\n/// attribute flags a repaint must reproduce. Clean-room; `Default` is the reset\n/// pen (`ESC[0m`).\n#[derive(Clone, Copy, PartialEq, Eq, Debug)]\nstruct Pen {\n    fg: Color,\n    bg: Color,\n    bold: bool,\n    dim: bool,\n    italic: bool,\n    underline: bool,\n    blink: bool,\n    reverse: bool,\n    hidden: bool,\n    strike: bool,\n}\n\nimpl Default for Pen {\n    fn default() -> Self {\n        Pen {\n            fg: Color::Default,\n            bg: Color::Default,\n            bold: false,\n            dim: false,\n            italic: false,\n            underline: false,\n            blink: false,\n            reverse: false,\n            hidden: false,\n            strike: false,\n        }\n    }\n}\n\nimpl Pen {\n    /// Encode this pen as a self-contained SGR sequence — a leading `0` reset so\n    /// the sequence fully specifies the state regardless of the terminal's prior\n    /// pen (correctness over minimality; a repaint is infrequent).\n    fn encode(&self) -> String {\n        let mut codes: Vec<String> = vec![\"0\".to_string()];\n        if self.bold {\n            codes.push(\"1\".to_string());\n        }\n        if self.dim {\n            codes.push(\"2\".to_string());\n        }\n        if self.italic {\n            codes.push(\"3\".to_string());\n        }\n        if self.underline {\n            codes.push(\"4\".to_string());\n        }\n        if self.blink {\n            codes.push(\"5\".to_string());\n        }\n        if self.reverse {\n            codes.push(\"7\".to_string());\n        }\n        if self.hidden {\n            codes.push(\"8\".to_string());\n        }\n        if self.strike {\n            codes.push(\"9\".to_string());\n        }\n        push_color(&mut codes, self.fg, 30, 90, 38);\n        push_color(&mut codes, self.bg, 40, 100, 48);\n        format!(\"\\x1b[{}m\", codes.join(\";\"))\n    }\n}\n\n/// Append the SGR codes for one colour channel. `basic` = the 30/40 base, `bright`\n/// = the 90/100 base, `ext` = the 38/48 extended selector.\nfn push_color(codes: &mut Vec<String>, color: Color, basic: u16, bright: u16, ext: u16) {\n    match color {\n        Color::Default => {}\n        Color::Idx(i) if i < 8 => codes.push((basic + i as u16).to_string()),\n        Color::Idx(i) if i < 16 => codes.push((bright + (i as u16 - 8)).to_string()),\n        Color::Idx(i) => {\n            codes.push(ext.to_string());\n            codes.push(\"5\".to_string());\n            codes.push(i.to_string());\n        }\n        Color::Rgb(r, g, b) => {\n            codes.push(ext.to_string());\n            codes.push(\"2\".to_string());\n            codes.push(r.to_string());\n            codes.push(g.to_string());\n            codes.push(b.to_string());\n        }\n    }\n}\n\n/// One screen cell: a character plus its pen.\n#[derive(Clone, Copy, PartialEq, Eq, Debug)]\nstruct Cell {\n    ch: char,\n    pen: Pen,\n}\n\nimpl Default for Cell {\n    fn default() -> Self {\n        Cell {\n            ch: ' ',\n            pen: Pen::default(),\n        }\n    }\n}\n\nimpl Cell {\n    /// A cell that renders as nothing on a freshly-cleared screen: a space with the\n    /// default pen. Trailing runs of these are trimmed from a repaint.\n    fn is_default_blank(&self) -> bool {\n        self.ch == ' ' && self.pen == Pen::default()\n    }\n}\n\n/// The public screen grid: a `vte` parser paired with the clean-room [`GridState`]\n/// it drives. Single-writer by construction (the broker's session drain thread) —\n/// mirrors the existing single-producer ring (ADR-0031, hazard 7.12).\npub struct ScreenGrid {\n    parser: Parser,\n    state: GridState,\n}\n\nimpl ScreenGrid {\n    /// A fresh grid sized `rows × cols` (a blank main screen, cursor home).\n    pub fn new(rows: u16, cols: u16) -> Self {\n        ScreenGrid {\n            parser: Parser::new(),\n            state: GridState::new(rows, cols),\n        }\n    }\n\n    /// Feed a raw output chunk through the parser, advancing the current screen.\n    /// Called on the session drain path AFTER the ring append + fan-out, so it adds\n    /// no latency to controller/viewer delivery (ADR-0031, REQ-HAZARD-INJECT-CONTROL-COEXIST).\n    pub fn advance(&mut self, bytes: &[u8]) {\n        for &b in bytes {\n            // Disjoint field borrows: the parser and the state it drives are\n            // separate fields of `self`, so this compiles without interior mutability.\n            self.parser.advance(&mut self.state, b);\n        }\n    }\n\n    /// Reallocate for a new geometry (a controller resize), preserving the\n    /// top-left overlap and clamping the cursor.\n    pub fn resize(&mut self, rows: u16, cols: u16) {\n        self.state.resize(rows, cols);\n    }\n\n    /// Synthesize a clean repaint of the CURRENT screen: a byte sequence that,\n    /// applied to a fresh terminal, reproduces the visible viewport (alt/main\n    /// buffer, cells with minimal-ish SGR runs, cursor position + visibility) with\n    /// NO raw scrollback and NO mid-stream rendering debris. This replaces the\n    /// raw-ring initial batch at `become_controller` / `add_viewer`.\n    pub fn render_repaint(&self) -> Vec<u8> {\n        self.state.render_repaint()\n    }\n}\n\n/// The clean-room terminal state machine: cell buffers (main + alt), cursor,\n/// pen, modes, scroll region. Implements [`vte::Perform`] — every method is a VT\n/// action the parser hands us; we own the screen semantics.\nstruct GridState {\n    rows: usize,\n    cols: usize,\n    main: Vec<Vec<Cell>>,\n    alt: Vec<Vec<Cell>>,\n    alt_active: bool,\n    row: usize,\n    col: usize,\n    pen: Pen,\n    cursor_visible: bool,\n    pending_wrap: bool,\n    saved: Option<(usize, usize)>,\n    scroll_top: usize,\n    scroll_bottom: usize,\n    /// The window title the child last set (OSC 0/2) — replayed on repaint so a\n    /// cold attach carries the CURRENT title, not the attacher's stale one (E-2).\n    /// Cleared by RIS (full_reset rebuilds the state); survives resize.\n    // [impl->REQ-BROKER-SCREEN-GRID]\n    title: Option<String>,\n}\n\nimpl GridState {\n    fn new(rows: u16, cols: u16) -> Self {\n        let rows = rows.max(1) as usize;\n        let cols = cols.max(1) as usize;\n        GridState {\n            rows,\n            cols,\n            main: blank_buffer(rows, cols),\n            alt: blank_buffer(rows, cols),\n            alt_active: false,\n            row: 0,\n            col: 0,\n            pen: Pen::default(),\n            cursor_visible: true,\n            pending_wrap: false,\n            saved: None,\n            scroll_top: 0,\n            scroll_bottom: rows - 1,\n            title: None,\n        }\n    }\n\n    fn buf(&self) -> &Vec<Vec<Cell>> {\n        if self.alt_active {\n            &self.alt\n        } else {\n            &self.main\n        }\n    }\n\n    fn buf_mut(&mut self) -> &mut Vec<Vec<Cell>> {\n        if self.alt_active {\n            &mut self.alt\n        } else {\n            &mut self.main\n        }\n    }\n\n    /// The pen an erase fills with: attributes cleared but the current background\n    /// preserved (so a status-bar erase keeps its colour).\n    fn erase_pen(&self) -> Pen {\n        Pen {\n            bg: self.pen.bg,\n            ..Pen::default()\n        }\n    }\n\n…\n}\n\n[Showing lines 1-263 of 1023. Use :264 to continue]"}],"details":{"truncation":{"content":"//! Server-side **screen grid** — the clean-room render model behind the clean\r\n//! repaint on attach (field bug #6 / `REQ-BROKER-SCREEN-GRID`, ADR-0031).\r\n//!\r\n//! A cold attach used to stream the entire retained raw-byte ring as its initial\r\n//! batch. For an alt-screen TUI that ring is a *rendering protocol mid-stream*\r\n//! (alt-screen enter/exit, absolute cursor moves, repaints, a torn oldest chunk);\r\n//! replayed raw into a fresh terminal it corrupts scrollback (#6) and leaves stray\r\n//! cells (#7) / resize debris (#8). The [`ScreenGrid`] interprets the same byte\r\n//! stream into an authoritative **current screen** and synthesizes ONE clean\r\n//! [`ScreenGrid::render_repaint`] the client can paint into a fresh terminal.\r\n//!\r\n//! **Scope (ratified, ADR-0031):** current screen only — the visible viewport\r\n//! (cell buffer + cursor + main/alt buffer + the modes a repaint needs). Scrollback\r\n//! stays the raw ring (history). The grid drives ONLY the attach repaint; live\r\n//! frames after attach are the unchanged raw bytes.\r\n//!\r\n//! **Provenance (ADR-0001 / ADR-0031):** the byte→action layer is the vetted `vte`\r\n//! parser state machine (the one non-clean-room dep — a universal, spec-fragile VT\r\n//! machine where a hand-roll would reproduce the exact artifacts #6 kills). The\r\n//! grid semantics on top ([`GridState`] impl of [`vte::Perform`]) are clean-room.\r\n\r\nuse vte::{Params, Parser, Perform};\r\n\r\n/// A cell colour: the terminal default, a palette index (0–15 basic/bright +\r\n/// 16–255 the 256-colour cube), or 24-bit truecolour.\r\n#[derive(Clone, Copy, PartialEq, Eq, Debug)]\r\nenum Color {\r\n    Default,\r\n    Idx(u8),\r\n    Rgb(u8, u8, u8),\r\n}\r\n\r\n/// The graphic rendition (SGR) state of a cell — foreground/background plus the\r\n/// attribute flags a repaint must reproduce. Clean-room; `Default` is the reset\r\n/// pen (`ESC[0m`).\r\n#[derive(Clone, Copy, PartialEq, Eq, Debug)]\r\nstruct Pen {\r\n    fg: Color,\r\n    bg: Color,\r\n    bold: bool,\r\n    dim: bool,\r\n    italic: bool,\r\n    underline: bool,\r\n    blink: bool,\r\n    reverse: bool,\r\n    hidden: bool,\r\n    strike: bool,\r\n}\r\n\r\nimpl Default for Pen {\r\n    fn default() -> Self {\r\n        Pen {\r\n            fg: Color::Default,\r\n            bg: Color::Default,\r\n            bold: false,\r\n            dim: false,\r\n            italic: false,\r\n            underline: false,\r\n            blink: false,\r\n            reverse: false,\r\n            hidden: false,\r\n            strike: false,\r\n        }\r\n    }\r\n}\r\n\r\nimpl Pen {\r\n    /// Encode this pen as a self-contained SGR sequence — a leading `0` reset so\r\n    /// the sequence fully specifies the state regardless of the terminal's prior\r\n    /// pen (correctness over minimality; a repaint is infrequent).\r\n    fn encode(&self) -> String {\r\n        let mut codes: Vec<String> = vec![\"0\".to_string()];\r\n        if self.bold {\r\n            codes.push(\"1\".to_string());\r\n        }\r\n        if self.dim {\r\n            codes.push(\"2\".to_string());\r\n        }\r\n        if self.italic {\r\n            codes.push(\"3\".to_string());\r\n        }\r\n        if self.underline {\r\n            codes.push(\"4\".to_string());\r\n        }\r\n        if self.blink {\r\n            codes.push(\"5\".to_string());\r\n        }\r\n        if self.reverse {\r\n            codes.push(\"7\".to_string());\r\n        }\r\n        if self.hidden {\r\n            codes.push(\"8\".to_string());\r\n        }\r\n        if self.strike {\r\n            codes.push(\"9\".to_string());\r\n        }\r\n        push_color(&mut codes, self.fg, 30, 90, 38);\r\n        push_color(&mut codes, self.bg, 40, 100, 48);\r\n        format!(\"\\x1b[{}m\", codes.join(\";\"))\r\n    }\r\n}\r\n\r\n/// Append the SGR codes for one colour channel. `basic` = the 30/40 base, `bright`\r\n/// = the 90/100 base, `ext` = the 38/48 extended selector.\r\nfn push_color(codes: &mut Vec<String>, color: Color, basic: u16, bright: u16, ext: u16) {\r\n    match color {\r\n        Color::Default => {}\r\n        Color::Idx(i) if i < 8 => codes.push((basic + i as u16).to_string()),\r\n        Color::Idx(i) if i < 16 => codes.push((bright + (i as u16 - 8)).to_string()),\r\n        Color::Idx(i) => {\r\n            codes.push(ext.to_string());\r\n            codes.push(\"5\".to_string());\r\n            codes.push(i.to_string());\r\n        }\r\n        Color::Rgb(r, g, b) => {\r\n            codes.push(ext.to_string());\r\n            codes.push(\"2\".to_string());\r\n            codes.push(r.to_string());\r\n            codes.push(g.to_string());\r\n            codes.push(b.to_string());\r\n        }\r\n    }\r\n}\r\n\r\n/// One screen cell: a character plus its pen.\r\n#[derive(Clone, Copy, PartialEq, Eq, Debug)]\r\nstruct Cell {\r\n    ch: char,\r\n    pen: Pen,\r\n}\r\n\r\nimpl Default for Cell {\r\n    fn default() -> Self {\r\n        Cell {\r\n            ch: ' ',\r\n            pen: Pen::default(),\r\n        }\r\n    }\r\n}\r\n\r\nimpl Cell {\r\n    /// A cell that renders as nothing on a freshly-cleared screen: a space with the\r\n    /// default pen. Trailing runs of these are trimmed from a repaint.\r\n    fn is_default_blank(&self) -> bool {\r\n        self.ch == ' ' && self.pen == Pen::default()\r\n    }\r\n}\r\n\r\n/// The public screen grid: a `vte` parser paired with the clean-room [`GridState`]\r\n/// it drives. Single-writer by construction (the broker's session drain thread) —\r\n/// mirrors the existing single-producer ring (ADR-0031, hazard 7.12).\r\npub struct ScreenGrid {\r\n    parser: Parser,\r\n    state: GridState,\r\n}\r\n\r\nimpl ScreenGrid {\r\n    /// A fresh grid sized `rows × cols` (a blank main screen, cursor home).\r\n    pub fn new(rows: u16, cols: u16) -> Self {\r\n        ScreenGrid {\r\n            parser: Parser::new(),\r\n            state: GridState::new(rows, cols),\r\n        }\r\n    }\r\n\r\n    /// Feed a raw output chunk through the parser, advancing the current screen.\r\n    /// Called on the session drain path AFTER the ring append + fan-out, so it adds\r\n    /// no latency to controller/viewer delivery (ADR-0031, REQ-HAZARD-INJECT-CONTROL-COEXIST).\r\n    pub fn advance(&mut self, bytes: &[u8]) {\r\n        for &b in bytes {\r\n            // Disjoint field borrows: the parser and the state it drives are\r\n            // separate fields of `self`, so this compiles without interior mutability.\r\n            self.parser.advance(&mut self.state, b);\r\n        }\r\n    }\r\n\r\n    /// Reallocate for a new geometry (a controller resize), preserving the\r\n    /// top-left overlap and clamping the cursor.\r\n    pub fn resize(&mut self, rows: u16, cols: u16) {\r\n        self.state.resize(rows, cols);\r\n    }\r\n\r\n    /// Synthesize a clean repaint of the CURRENT screen: a byte sequence that,\r\n    /// applied to a fresh terminal, reproduces the visible viewport (alt/main\r\n    /// buffer, cells with minimal-ish SGR runs, cursor position + visibility) with\r\n    /// NO raw scrollback and NO mid-stream rendering debris. This replaces the\r\n    /// raw-ring initial batch at `become_controller` / `add_viewer`.\r\n    pub fn render_repaint(&self) -> Vec<u8> {\r\n        self.state.render_repaint()\r\n    }\r\n}\r\n\r\n/// The clean-room terminal state machine: cell buffers (main + alt), cursor,\r\n/// pen, modes, scroll region. Implements [`vte::Perform`] — every method is a VT\r\n/// action the parser hands us; we own the screen semantics.\r\nstruct GridState {\r\n    rows: usize,\r\n    cols: usize,\r\n    main: Vec<Vec<Cell>>,\r\n    alt: Vec<Vec<Cell>>,\r\n    alt_active: bool,\r\n    row: usize,\r\n    col: usize,\r\n    pen: Pen,\r\n    cursor_visible: bool,\r\n    pending_wrap: bool,\r\n    saved: Option<(usize, usize)>,\r\n    scroll_top: usize,\r\n    scroll_bottom: usize,\r\n    /// The window title the child last set (OSC 0/2) — replayed on repaint so a\r\n    /// cold attach carries the CURRENT title, not the attacher's stale one (E-2).\r\n    /// Cleared by RIS (full_reset rebuilds the state); survives resize.\r\n    // [impl->REQ-BROKER-SCREEN-GRID]\r\n    title: Option<String>,\r\n}\r\n\r\nimpl GridState {\r\n    fn new(rows: u16, cols: u16) -> Self {\r\n        let rows = rows.max(1) as usize;\r\n        let cols = cols.max(1) as usize;\r\n        GridState {\r\n            rows,\r\n            cols,\r\n            main: blank_buffer(rows, cols),\r\n            alt: blank_buffer(rows, cols),\r\n            alt_active: false,\r\n            row: 0,\r\n            col: 0,\r\n            pen: Pen::default(),\r\n            cursor_visible: true,\r\n            pending_wrap: false,\r\n            saved: None,\r\n            scroll_top: 0,\r\n            scroll_bottom: rows - 1,\r\n            title: None,\r\n        }\r\n    }\r\n\r\n    fn buf(&self) -> &Vec<Vec<Cell>> {\r\n        if self.alt_active {\r\n            &self.alt\r\n        } else {\r\n            &self.main\r\n        }\r\n    }\r\n\r\n    fn buf_mut(&mut self) -> &mut Vec<Vec<Cell>> {\r\n        if self.alt_active {\r\n            &mut self.alt\r\n        } else {\r\n            &mut self.main\r\n        }\r\n    }\r\n\r\n    /// The pen an erase fills with: attributes cleared but the current background\r\n    /// preserved (so a status-bar erase keeps its colour).\r\n    fn erase_pen(&self) -> Pen {\r\n        Pen {\r\n            bg: self.pen.bg,\r\n            ..Pen::default()\r\n        }\r\n    }\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":1023,"totalBytes":9054,"outputLines":263,"outputBytes":9054,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Server-side **screen grid** — the clean-room render model behind the clean\r\n//! repaint on attach (field bug #6 / `REQ-BROKER-SCREEN-GRID`, ADR-0031).\r\n//!\r\n//! A cold attach used to stream the entire retained raw-byte ring as its initial\r\n//! batch. For an alt-screen TUI that ring is a *rendering protocol mid-stream*\r\n//! (alt-screen enter/exit, absolute cursor moves, repaints, a torn oldest chunk);\r\n//! replayed raw into a fresh terminal it corrupts scrollback (#6) and leaves stray\r\n//! cells (#7) / resize debris (#8). The [`ScreenGrid`] interprets the same byte\r\n//! stream into an authoritative **current screen** and synthesizes ONE clean\r\n//! [`ScreenGrid::render_repaint`] the client can paint into a fresh terminal.\r\n//!\r\n//! **Scope (ratified, ADR-0031):** current screen only — the visible viewport\r\n//! (cell buffer + cursor + main/alt buffer + the modes a repaint needs). Scrollback\r\n//! stays the raw ring (history). The grid drives ONLY the attach repaint; live\r\n//! frames after attach are the unchanged raw bytes.\r\n//!\r\n//! **Provenance (ADR-0001 / ADR-0031):** the byte→action layer is the vetted `vte`\r\n//! parser state machine (the one non-clean-room dep — a universal, spec-fragile VT\r\n//! machine where a hand-roll would reproduce the exact artifacts #6 kills). The\r\n//! grid semantics on top ([`GridState`] impl of [`vte::Perform`]) are clean-room.\r\n\r\nuse vte::{Params, Parser, Perform};\r\n\r\n/// A cell colour: the terminal default, a palette index (0–15 basic/bright +\r\n/// 16–255 the 256-colour cube), or 24-bit truecolour.\r\n#[derive(Clone, Copy, PartialEq, Eq, Debug)]\r\nenum Color {\r\n    Default,\r\n    Idx(u8),\r\n    Rgb(u8, u8, u8),\r\n}\r\n\r\n/// The graphic rendition (SGR) state of a cell — foreground/background plus the\r\n/// attribute flags a repaint must reproduce. Clean-room; `Default` is the reset\r\n/// pen (`ESC[0m`).\r\n#[derive(Clone, Copy, PartialEq, Eq, Debug)]\r\nstruct Pen {\r\n    fg: Color,\r\n    bg: Color,\r\n    bold: bool,\r\n    dim: bool,\r\n    italic: bool,\r\n    underline: bool,\r\n    blink: bool,\r\n    reverse: bool,\r\n    hidden: bool,\r\n    strike: bool,\r\n}\r\n\r\nimpl Default for Pen {\r\n    fn default() -> Self {\r\n        Pen {\r\n            fg: Color::Default,\r\n            bg: Color::Default,\r\n            bold: false,\r\n            dim: false,\r\n            italic: false,\r\n            underline: false,\r\n            blink: false,\r\n            reverse: false,\r\n            hidden: false,\r\n            strike: false,\r\n        }\r\n    }\r\n}\r\n\r\nimpl Pen {\r\n    /// Encode this pen as a self-contained SGR sequence — a leading `0` reset so\r\n    /// the sequence fully specifies the state regardless of the terminal's prior\r\n    /// pen (correctness over minimality; a repaint is infrequent).\r\n    fn encode(&self) -> String {\r\n        let mut codes: Vec<String> = vec![\"0\".to_string()];\r\n        if self.bold {\r\n            codes.push(\"1\".to_string());\r\n        }\r\n        if self.dim {\r\n            codes.push(\"2\".to_string());\r\n        }\r\n        if self.italic {\r\n            codes.push(\"3\".to_string());\r\n        }\r\n        if self.underline {\r\n            codes.push(\"4\".to_string());\r\n        }\r\n        if self.blink {\r\n            codes.push(\"5\".to_string());\r\n        }\r\n        if self.reverse {\r\n            codes.push(\"7\".to_string());\r\n        }\r\n        if self.hidden {\r\n            codes.push(\"8\".to_string());\r\n        }\r\n        if self.strike {\r\n            codes.push(\"9\".to_string());\r\n        }\r\n        push_color(&mut codes, self.fg, 30, 90, 38);\r\n        push_color(&mut codes, self.bg, 40, 100, 48);\r\n        format!(\"\\x1b[{}m\", codes.join(\";\"))\r\n    }\r\n}\r\n\r\n/// Append the SGR codes for one colour channel. `basic` = the 30/40 base, `bright`\r\n/// = the 90/100 base, `ext` = the 38/48 extended selector.\r\nfn push_color(codes: &mut Vec<String>, color: Color, basic: u16, bright: u16, ext: u16) {\r\n    match color {\r\n        Color::Default => {}\r\n        Color::Idx(i) if i < 8 => codes.push((basic + i as u16).to_string()),\r\n        Color::Idx(i) if i < 16 => codes.push((bright + (i as u16 - 8)).to_string()),\r\n        Color::Idx(i) => {\r\n            codes.push(ext.to_string());\r\n            codes.push(\"5\".to_string());\r\n            codes.push(i.to_string());\r\n        }\r\n        Color::Rgb(r, g, b) => {\r\n            codes.push(ext.to_string());\r\n            codes.push(\"2\".to_string());\r\n            codes.push(r.to_string());\r\n            codes.push(g.to_string());\r\n            codes.push(b.to_string());\r\n        }\r\n    }\r\n}\r\n\r\n/// One screen cell: a character plus its pen.\r\n#[derive(Clone, Copy, PartialEq, Eq, Debug)]\r\nstruct Cell {\r\n    ch: char,\r\n    pen: Pen,\r\n}\r\n\r\nimpl Default for Cell {\r\n    fn default() -> Self {\r\n        Cell {\r\n            ch: ' ',\r\n            pen: Pen::default(),\r\n        }\r\n    }\r\n}\r\n\r\nimpl Cell {\r\n    /// A cell that renders as nothing on a freshly-cleared screen: a space with the\r\n    /// default pen. Trailing runs of these are trimmed from a repaint.\r\n    fn is_default_blank(&self) -> bool {\r\n        self.ch == ' ' && self.pen == Pen::default()\r\n    }\r\n}\r\n\r\n/// The public screen grid: a `vte` parser paired with the clean-room [`GridState`]\r\n/// it drives. Single-writer by construction (the broker's session drain thread) —\r\n/// mirrors the existing single-producer ring (ADR-0031, hazard 7.12).\r\npub struct ScreenGrid {\r\n    parser: Parser,\r\n    state: GridState,\r\n}\r\n\r\nimpl ScreenGrid {\r\n    /// A fresh grid sized `rows × cols` (a blank main screen, cursor home).\r\n    pub fn new(rows: u16, cols: u16) -> Self {\r\n        ScreenGrid {\r\n            parser: Parser::new(),\r\n            state: GridState::new(rows, cols),\r\n        }\r\n    }\r\n\r\n    /// Feed a raw output chunk through the parser, advancing the current screen.\r\n    /// Called on the session drain path AFTER the ring append + fan-out, so it adds\r\n    /// no latency to controller/viewer delivery (ADR-0031, REQ-HAZARD-INJECT-CONTROL-COEXIST).\r\n    pub fn advance(&mut self, bytes: &[u8]) {\r\n        for &b in bytes {\r\n            // Disjoint field borrows: the parser and the state it drives are\r\n            // separate fields of `self`, so this compiles without interior mutability.\r\n            self.parser.advance(&mut self.state, b);\r\n        }\r\n    }\r\n\r\n    /// Reallocate for a new geometry (a controller resize), preserving the\r\n    /// top-left overlap and clamping the cursor.\r\n    pub fn resize(&mut self, rows: u16, cols: u16) {\r\n        self.state.resize(rows, cols);\r\n    }\r\n\r\n    /// Synthesize a clean repaint of the CURRENT screen: a byte sequence that,\r\n    /// applied to a fresh terminal, reproduces the visible viewport (alt/main\r\n    /// buffer, cells with minimal-ish SGR runs, cursor position + visibility) with\r\n    /// NO raw scrollback and NO mid-stream rendering debris. This replaces the\r\n    /// raw-ring initial batch at `become_controller` / `add_viewer`.\r\n    pub fn render_repaint(&self) -> Vec<u8> {\r\n        self.state.render_repaint()\r\n    }\r\n}\r\n\r\n/// The clean-room terminal state machine: cell buffers (main + alt), cursor,\r\n/// pen, modes, scroll region. Implements [`vte::Perform`] — every method is a VT\r\n/// action the parser hands us; we own the screen semantics.\r\nstruct GridState {\r\n    rows: usize,\r\n    cols: usize,\r\n    main: Vec<Vec<Cell>>,\r\n    alt: Vec<Vec<Cell>>,\r\n    alt_active: bool,\r\n    row: usize,\r\n    col: usize,\r\n    pen: Pen,\r\n    cursor_visible: bool,\r\n    pending_wrap: bool,\r\n    saved: Option<(usize, usize)>,\r\n    scroll_top: usize,\r\n    scroll_bottom: usize,\r\n    /// The window title the child last set (OSC 0/2) — replayed on repaint so a\r\n    /// cold attach carries the CURRENT title, not the attacher's stale one (E-2).\r\n    /// Cleared by RIS (full_reset rebuilds the state); survives resize.\r\n    // [impl->REQ-BROKER-SCREEN-GRID]\r\n    title: Option<String>,\r\n}\r\n\r\nimpl GridState {\r\n    fn new(rows: u16, cols: u16) -> Self {\r\n        let rows = rows.max(1) as usize;\r\n        let cols = cols.max(1) as usize;\r\n        GridState {\r\n            rows,\r\n            cols,\r\n            main: blank_buffer(rows, cols),\r\n            alt: blank_buffer(rows, cols),\r\n            alt_active: false,\r\n            row: 0,\r\n            col: 0,\r\n            pen: Pen::default(),\r\n            cursor_visible: true,\r\n            pending_wrap: false,\r\n            saved: None,\r\n            scroll_top: 0,\r\n            scroll_bottom: rows - 1,\r\n            title: None,\r\n        }\r\n    }\r\n\r\n    fn buf(&self) -> &Vec<Vec<Cell>> {\r\n        if self.alt_active {\r\n            &self.alt\r\n        } else {\r\n            &self.main\r\n        }\r\n    }\r\n\r\n    fn buf_mut(&mut self) -> &mut Vec<Vec<Cell>> {\r\n        if self.alt_active {\r\n            &mut self.alt\r\n        } else {\r\n            &mut self.main\r\n        }\r\n    }\r\n\r\n    /// The pen an erase fills with: attributes cleared but the current background\r\n    /// preserved (so a status-bar erase keeps its colour).\r\n    fn erase_pen(&self) -> Pen {\r\n        Pen {\r\n            bg: self.pen.bg,\r\n            ..Pen::default()\r\n        }\r\n    }\r\n\r\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,null,624]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\screen.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1023,"totalBytes":9054,"outputLines":263,"outputBytes":9054,"shownRange":{"start":1,"end":263},"nextOffset":264}}},"isError":false,"timestamp":1784365494700}}
{"type":"message","id":"ba023f8d","parentId":"21aed983","timestamp":"2026-07-18T09:04:54.708Z","message":{"role":"toolResult","toolCallId":"call_5Dxo4zqUFn3cKSkelKmAxZzc|fc_0ce5441d29b9f05d016a5b41b69c0c819bb9617e2fa593445b","toolName":"read","content":[{"type":"text","text":"impl GridState {\n…\n    fn switch_alt(&mut self, enter: bool, save_restore: bool) {\n…\n        if enter {\n…\n            self.pending_wrap = false;\n        } else {\n            self.alt_active = false;\n            if save_restore {\n                if let Some((r, c)) = self.saved.take() {\n                    self.row = r;\n                    self.col = c;\n                }\n            }\n            self.pending_wrap = false;\n            self.clamp_cursor();\n        }\n    }\n\n    fn apply_sgr(&mut self, params: &Params) {\n        let items: Vec<&[u16]> = params.iter().collect();\n        if items.is_empty() {\n            self.pen = Pen::default();\n            return;\n        }\n        let mut i = 0;\n        while i < items.len() {\n            let item = items[i];\n            let code = item.first().copied().unwrap_or(0);\n            match code {\n                0 => self.pen = Pen::default(),\n                1 => self.pen.bold = true,\n                2 => self.pen.dim = true,\n                3 => self.pen.italic = true,\n                4 => self.pen.underline = true,\n                5 => self.pen.blink = true,\n                7 => self.pen.reverse = true,\n                8 => self.pen.hidden = true,\n                9 => self.pen.strike = true,\n                22 => {\n                    self.pen.bold = false;\n                    self.pen.dim = false;\n                }\n                23 => self.pen.italic = false,\n                24 => self.pen.underline = false,\n                25 => self.pen.blink = false,\n                27 => self.pen.reverse = false,\n                28 => self.pen.hidden = false,\n                29 => self.pen.strike = false,\n                30..=37 => self.pen.fg = Color::Idx((code - 30) as u8),\n                39 => self.pen.fg = Color::Default,\n                40..=47 => self.pen.bg = Color::Idx((code - 40) as u8),\n                49 => self.pen.bg = Color::Default,\n                90..=97 => self.pen.fg = Color::Idx((code - 90 + 8) as u8),\n                100..=107 => self.pen.bg = Color::Idx((code - 100 + 8) as u8),\n                38 => self.pen.fg = extended_color(&items, &mut i).unwrap_or(self.pen.fg),\n                48 => self.pen.bg = extended_color(&items, &mut i).unwrap_or(self.pen.bg),\n                _ => {}\n            }\n            i += 1;\n        }\n    }\n\n    fn resize(&mut self, rows: u16, cols: u16) {\n        let rows = rows.max(1) as usize;\n        let cols = cols.max(1) as usize;\n        self.main = resized_buffer(&self.main, rows, cols);\n        self.alt = resized_buffer(&self.alt, rows, cols);\n        self.rows = rows;\n        self.cols = cols;\n        self.scroll_top = 0;\n        self.scroll_bottom = rows - 1;\n        self.pending_wrap = false;\n        self.clamp_cursor();\n    }\n\n    fn full_reset(&mut self) {\n        let (rows, cols) = (self.rows, self.cols);\n        *self = GridState::new(rows as u16, cols as u16);\n    }\n\n    // --- Repaint ------------------------------------------------------------\n\n    fn render_repaint(&self) -> Vec<u8> {\n        let mut out = String::new();\n        // Put the client on the matching buffer so its own scrollback is never\n        // touched: 1049h saves+switches+clears the alt screen; 1049l returns to main.\n        if self.alt_active {\n            out.push_str(\"\\x1b[?1049h\");\n        } else {\n            out.push_str(\"\\x1b[?1049l\");\n        }\n        // Replay the child's last-set window title (OSC 0/2) so a cold attach\n        // shows the CURRENT title, not the stale pre-attach one (E-2). BEL\n        // terminator — the widest-compat form.\n        // [impl->REQ-BROKER-SCREEN-GRID]\n        if let Some(title) = &self.title {\n            out.push_str(&format!(\"\\x1b]2;{title}\\x07\"));\n        }\n        out.push_str(\"\\x1b[0m\"); // reset pen\n        out.push_str(\"\\x1b[2J\"); // clear the (now-active) screen\n        out.push_str(\"\\x1b[H\"); // home\n        let mut emitted = Pen::default();\n        let buf = self.buf();\n        for (r, line) in buf.iter().enumerate() {\n            // Trim trailing default-blank cells — the screen is already cleared,\n            // so only paint what carries content or a non-default background.\n            let last = line.iter().rposition(|c| !c.is_default_blank());\n            let Some(last) = last else { continue };\n            out.push_str(&format!(\"\\x1b[{};1H\", r + 1));\n            for cell in &line[..=last] {\n                if cell.pen != emitted {\n                    out.push_str(&cell.pen.encode());\n                    emitted = cell.pen;\n                }\n                out.push(cell.ch);\n            }\n        }\n        // Restore the child's live pen so the next raw frames render with the SGR\n        // state the child assumes is current.\n        out.push_str(&self.pen.encode());\n        out.push_str(if self.cursor_visible {\n            \"\\x1b[?25h\"\n        } else {\n            \"\\x1b[?25l\"\n        });\n        // Cursor last, at its live position.\n        out.push_str(&format!(\"\\x1b[{};{}H\", self.row + 1, self.col + 1));\n        out.into_bytes()\n    }\n}\n\n/// Parse a `38;5;n` / `38;2;r;g;b` (or the colon-subparam form `38:5:n` /\n/// `38:2:r:g:b`) extended colour, advancing the semicolon index past any params it\n/// consumes. Returns `None` on a malformed selector.\nfn extended_color(items: &[&[u16]], i: &mut usize) -> Option<Color> {\n    let item = items[*i];\n    if item.len() >= 2 {\n        // Colon-subparam form: everything is in this one param slice.\n        match item[1] {\n            5 => item.get(2).map(|&n| Color::Idx(n as u8)),\n            2 => {\n                // 38:2:r:g:b, or 38:2:cs:r:g:b — take the last three components.\n                let n = item.len();\n                if n >= 5 {\n                    Some(Color::Rgb(\n                        item[n - 3] as u8,\n                        item[n - 2] as u8,\n                        item[n - 1] as u8,\n                    ))\n                } else {\n                    None\n                }\n            }\n            _ => None,\n        }\n    } else {\n        // Semicolon form: read the mode + components from the following params.\n        let mode = items.get(*i + 1).and_then(|p| p.first()).copied();\n        match mode {\n            Some(5) => {\n                let n = items.get(*i + 2).and_then(|p| p.first()).copied();\n                *i += 2;\n                n.map(|n| Color::Idx(n as u8))\n            }\n            Some(2) => {\n                let r = items.get(*i + 2).and_then(|p| p.first()).copied();\n                let g = items.get(*i + 3).and_then(|p| p.first()).copied();\n                let b = items.get(*i + 4).and_then(|p| p.first()).copied();\n                *i += 4;\n                match (r, g, b) {\n                    (Some(r), Some(g), Some(b)) => Some(Color::Rgb(r as u8, g as u8, b as u8)),\n                    _ => None,\n                }\n            }\n            _ => None,\n        }\n    }\n}\n\nfn blank_buffer(rows: usize, cols: usize) -> Vec<Vec<Cell>> {\n    vec![vec![Cell::default(); cols]; rows]\n}\n\n/// Reallocate a buffer to `rows × cols`, copying the overlapping top-left region.\nfn resized_buffer(old: &[Vec<Cell>], rows: usize, cols: usize) -> Vec<Vec<Cell>> {\n    let mut buf = blank_buffer(rows, cols);\n    for (r, line) in old.iter().take(rows).enumerate() {\n        for (c, cell) in line.iter().take(cols).enumerate() {\n            buf[r][c] = *cell;\n        }\n    }\n    buf\n}\n\n/// First sub-value of the `n`th CSI param, or `default` when absent/zero-omitted.\nfn ps(params: &Params, n: usize, default: u16) -> u16 {\n    params\n        .iter()\n        .nth(n)\n        .and_then(|p| p.first())\n        .copied()\n        .filter(|&v| v != 0)\n        .unwrap_or(default)\n}\n\nimpl Perform for GridState {\n    fn print(&mut self, c: char) {\n        self.put_char(c);\n    }\n\n    fn execute(&mut self, byte: u8) {\n        match byte {\n            0x08 => {\n                // BS\n                self.pending_wrap = false;\n                if self.col > 0 {\n                    self.col -= 1;\n                }\n            }\n            0x09 => {\n                // HT — next multiple of 8, clamped\n                self.pending_wrap = false;\n                let next = ((self.col / 8) + 1) * 8;\n                self.col = next.min(self.cols - 1);\n            }\n            0x0A..=0x0C => {\n                // LF / VT / FF\n                self.pending_wrap = false;\n                self.line_feed();\n            }\n            0x0D => {\n                // CR\n                self.pending_wrap = false;\n                self.col = 0;\n            }\n            _ => {}\n        }\n    }\n\n    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\n        let private = intermediates.first() == Some(&b'?');\n        match action {\n            'A' => {\n                self.pending_wrap = false;\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\n            }\n            'B' | 'e' => {\n                self.pending_wrap = false;\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\n            }\n            'C' | 'a' => {\n                self.pending_wrap = false;\n                self.col = (self.col + ps(params, 0, 1) as usize).min(self.cols - 1);\n            }\n            'D' => {\n                self.pending_wrap = false;\n                self.col = self.col.saturating_sub(ps(params, 0, 1) as usize);\n            }\n            'E' => {\n                self.pending_wrap = false;\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\n                self.col = 0;\n            }\n            'F' => {\n                self.pending_wrap = false;\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\n                self.col = 0;\n            }\n            'G' | '`' => {\n                self.pending_wrap = false;\n                self.col = (ps(params, 0, 1) as usize - 1).min(self.cols - 1);\n            }\n            'd' => {\n                self.pending_wrap = false;\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\n            }\n            'H' | 'f' => {\n                self.pending_wrap = false;\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\n                self.col = (ps(params, 1, 1) as usize - 1).min(self.cols - 1);\n            }\n            'J' => self.erase_display(ps(params, 0, 0)),\n            'K' => self.erase_line(ps(params, 0, 0)),\n            'X' => self.erase_chars(ps(params, 0, 1) as usize),\n            '@' => self.insert_chars(ps(params, 0, 1) as usize),\n            'P' => self.delete_chars(ps(params, 0, 1) as usize),\n            'L' => self.insert_lines(ps(params, 0, 1) as usize),\n            'M' => self.delete_lines(ps(params, 0, 1) as usize),\n            'S' => self.scroll_up(ps(params, 0, 1) as usize),\n…\n        }\n    }\n…\n}\n\n[Showing lines 499-783 of 1023. Use :784 to continue]"}],"details":{"truncation":{"content":"            self.pending_wrap = false;\r\n        } else {\r\n            self.alt_active = false;\r\n            if save_restore {\r\n                if let Some((r, c)) = self.saved.take() {\r\n                    self.row = r;\r\n                    self.col = c;\r\n                }\r\n            }\r\n            self.pending_wrap = false;\r\n            self.clamp_cursor();\r\n        }\r\n    }\r\n\r\n    fn apply_sgr(&mut self, params: &Params) {\r\n        let items: Vec<&[u16]> = params.iter().collect();\r\n        if items.is_empty() {\r\n            self.pen = Pen::default();\r\n            return;\r\n        }\r\n        let mut i = 0;\r\n        while i < items.len() {\r\n            let item = items[i];\r\n            let code = item.first().copied().unwrap_or(0);\r\n            match code {\r\n                0 => self.pen = Pen::default(),\r\n                1 => self.pen.bold = true,\r\n                2 => self.pen.dim = true,\r\n                3 => self.pen.italic = true,\r\n                4 => self.pen.underline = true,\r\n                5 => self.pen.blink = true,\r\n                7 => self.pen.reverse = true,\r\n                8 => self.pen.hidden = true,\r\n                9 => self.pen.strike = true,\r\n                22 => {\r\n                    self.pen.bold = false;\r\n                    self.pen.dim = false;\r\n                }\r\n                23 => self.pen.italic = false,\r\n                24 => self.pen.underline = false,\r\n                25 => self.pen.blink = false,\r\n                27 => self.pen.reverse = false,\r\n                28 => self.pen.hidden = false,\r\n                29 => self.pen.strike = false,\r\n                30..=37 => self.pen.fg = Color::Idx((code - 30) as u8),\r\n                39 => self.pen.fg = Color::Default,\r\n                40..=47 => self.pen.bg = Color::Idx((code - 40) as u8),\r\n                49 => self.pen.bg = Color::Default,\r\n                90..=97 => self.pen.fg = Color::Idx((code - 90 + 8) as u8),\r\n                100..=107 => self.pen.bg = Color::Idx((code - 100 + 8) as u8),\r\n                38 => self.pen.fg = extended_color(&items, &mut i).unwrap_or(self.pen.fg),\r\n                48 => self.pen.bg = extended_color(&items, &mut i).unwrap_or(self.pen.bg),\r\n                _ => {}\r\n            }\r\n            i += 1;\r\n        }\r\n    }\r\n\r\n    fn resize(&mut self, rows: u16, cols: u16) {\r\n        let rows = rows.max(1) as usize;\r\n        let cols = cols.max(1) as usize;\r\n        self.main = resized_buffer(&self.main, rows, cols);\r\n        self.alt = resized_buffer(&self.alt, rows, cols);\r\n        self.rows = rows;\r\n        self.cols = cols;\r\n        self.scroll_top = 0;\r\n        self.scroll_bottom = rows - 1;\r\n        self.pending_wrap = false;\r\n        self.clamp_cursor();\r\n    }\r\n\r\n    fn full_reset(&mut self) {\r\n        let (rows, cols) = (self.rows, self.cols);\r\n        *self = GridState::new(rows as u16, cols as u16);\r\n    }\r\n\r\n    // --- Repaint ------------------------------------------------------------\r\n\r\n    fn render_repaint(&self) -> Vec<u8> {\r\n        let mut out = String::new();\r\n        // Put the client on the matching buffer so its own scrollback is never\r\n        // touched: 1049h saves+switches+clears the alt screen; 1049l returns to main.\r\n        if self.alt_active {\r\n            out.push_str(\"\\x1b[?1049h\");\r\n        } else {\r\n            out.push_str(\"\\x1b[?1049l\");\r\n        }\r\n        // Replay the child's last-set window title (OSC 0/2) so a cold attach\r\n        // shows the CURRENT title, not the stale pre-attach one (E-2). BEL\r\n        // terminator — the widest-compat form.\r\n        // [impl->REQ-BROKER-SCREEN-GRID]\r\n        if let Some(title) = &self.title {\r\n            out.push_str(&format!(\"\\x1b]2;{title}\\x07\"));\r\n        }\r\n        out.push_str(\"\\x1b[0m\"); // reset pen\r\n        out.push_str(\"\\x1b[2J\"); // clear the (now-active) screen\r\n        out.push_str(\"\\x1b[H\"); // home\r\n        let mut emitted = Pen::default();\r\n        let buf = self.buf();\r\n        for (r, line) in buf.iter().enumerate() {\r\n            // Trim trailing default-blank cells — the screen is already cleared,\r\n            // so only paint what carries content or a non-default background.\r\n            let last = line.iter().rposition(|c| !c.is_default_blank());\r\n            let Some(last) = last else { continue };\r\n            out.push_str(&format!(\"\\x1b[{};1H\", r + 1));\r\n            for cell in &line[..=last] {\r\n                if cell.pen != emitted {\r\n                    out.push_str(&cell.pen.encode());\r\n                    emitted = cell.pen;\r\n                }\r\n                out.push(cell.ch);\r\n            }\r\n        }\r\n        // Restore the child's live pen so the next raw frames render with the SGR\r\n        // state the child assumes is current.\r\n        out.push_str(&self.pen.encode());\r\n        out.push_str(if self.cursor_visible {\r\n            \"\\x1b[?25h\"\r\n        } else {\r\n            \"\\x1b[?25l\"\r\n        });\r\n        // Cursor last, at its live position.\r\n        out.push_str(&format!(\"\\x1b[{};{}H\", self.row + 1, self.col + 1));\r\n        out.into_bytes()\r\n    }\r\n}\r\n\r\n/// Parse a `38;5;n` / `38;2;r;g;b` (or the colon-subparam form `38:5:n` /\r\n/// `38:2:r:g:b`) extended colour, advancing the semicolon index past any params it\r\n/// consumes. Returns `None` on a malformed selector.\r\nfn extended_color(items: &[&[u16]], i: &mut usize) -> Option<Color> {\r\n    let item = items[*i];\r\n    if item.len() >= 2 {\r\n        // Colon-subparam form: everything is in this one param slice.\r\n        match item[1] {\r\n            5 => item.get(2).map(|&n| Color::Idx(n as u8)),\r\n            2 => {\r\n                // 38:2:r:g:b, or 38:2:cs:r:g:b — take the last three components.\r\n                let n = item.len();\r\n                if n >= 5 {\r\n                    Some(Color::Rgb(\r\n                        item[n - 3] as u8,\r\n                        item[n - 2] as u8,\r\n                        item[n - 1] as u8,\r\n                    ))\r\n                } else {\r\n                    None\r\n                }\r\n            }\r\n            _ => None,\r\n        }\r\n    } else {\r\n        // Semicolon form: read the mode + components from the following params.\r\n        let mode = items.get(*i + 1).and_then(|p| p.first()).copied();\r\n        match mode {\r\n            Some(5) => {\r\n                let n = items.get(*i + 2).and_then(|p| p.first()).copied();\r\n                *i += 2;\r\n                n.map(|n| Color::Idx(n as u8))\r\n            }\r\n            Some(2) => {\r\n                let r = items.get(*i + 2).and_then(|p| p.first()).copied();\r\n                let g = items.get(*i + 3).and_then(|p| p.first()).copied();\r\n                let b = items.get(*i + 4).and_then(|p| p.first()).copied();\r\n                *i += 4;\r\n                match (r, g, b) {\r\n                    (Some(r), Some(g), Some(b)) => Some(Color::Rgb(r as u8, g as u8, b as u8)),\r\n                    _ => None,\r\n                }\r\n            }\r\n            _ => None,\r\n        }\r\n    }\r\n}\r\n\r\nfn blank_buffer(rows: usize, cols: usize) -> Vec<Vec<Cell>> {\r\n    vec![vec![Cell::default(); cols]; rows]\r\n}\r\n\r\n/// Reallocate a buffer to `rows × cols`, copying the overlapping top-left region.\r\nfn resized_buffer(old: &[Vec<Cell>], rows: usize, cols: usize) -> Vec<Vec<Cell>> {\r\n    let mut buf = blank_buffer(rows, cols);\r\n    for (r, line) in old.iter().take(rows).enumerate() {\r\n        for (c, cell) in line.iter().take(cols).enumerate() {\r\n            buf[r][c] = *cell;\r\n        }\r\n    }\r\n    buf\r\n}\r\n\r\n/// First sub-value of the `n`th CSI param, or `default` when absent/zero-omitted.\r\nfn ps(params: &Params, n: usize, default: u16) -> u16 {\r\n    params\r\n        .iter()\r\n        .nth(n)\r\n        .and_then(|p| p.first())\r\n        .copied()\r\n        .filter(|&v| v != 0)\r\n        .unwrap_or(default)\r\n}\r\n\r\nimpl Perform for GridState {\r\n    fn print(&mut self, c: char) {\r\n        self.put_char(c);\r\n    }\r\n\r\n    fn execute(&mut self, byte: u8) {\r\n        match byte {\r\n            0x08 => {\r\n                // BS\r\n                self.pending_wrap = false;\r\n                if self.col > 0 {\r\n                    self.col -= 1;\r\n                }\r\n            }\r\n            0x09 => {\r\n                // HT — next multiple of 8, clamped\r\n                self.pending_wrap = false;\r\n                let next = ((self.col / 8) + 1) * 8;\r\n                self.col = next.min(self.cols - 1);\r\n            }\r\n            0x0A..=0x0C => {\r\n                // LF / VT / FF\r\n                self.pending_wrap = false;\r\n                self.line_feed();\r\n            }\r\n            0x0D => {\r\n                // CR\r\n                self.pending_wrap = false;\r\n                self.col = 0;\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\r\n        let private = intermediates.first() == Some(&b'?');\r\n        match action {\r\n            'A' => {\r\n                self.pending_wrap = false;\r\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\r\n            }\r\n            'B' | 'e' => {\r\n                self.pending_wrap = false;\r\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\r\n            }\r\n            'C' | 'a' => {\r\n                self.pending_wrap = false;\r\n                self.col = (self.col + ps(params, 0, 1) as usize).min(self.cols - 1);\r\n            }\r\n            'D' => {\r\n                self.pending_wrap = false;\r\n                self.col = self.col.saturating_sub(ps(params, 0, 1) as usize);\r\n            }\r\n            'E' => {\r\n                self.pending_wrap = false;\r\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\r\n                self.col = 0;\r\n            }\r\n            'F' => {\r\n                self.pending_wrap = false;\r\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\r\n                self.col = 0;\r\n            }\r\n            'G' | '`' => {\r\n                self.pending_wrap = false;\r\n                self.col = (ps(params, 0, 1) as usize - 1).min(self.cols - 1);\r\n            }\r\n            'd' => {\r\n                self.pending_wrap = false;\r\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\r\n            }\r\n            'H' | 'f' => {\r\n                self.pending_wrap = false;\r\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\r\n                self.col = (ps(params, 1, 1) as usize - 1).min(self.cols - 1);\r\n            }\r\n            'J' => self.erase_display(ps(params, 0, 0)),\r\n            'K' => self.erase_line(ps(params, 0, 0)),\r\n            'X' => self.erase_chars(ps(params, 0, 1) as usize),\r\n            '@' => self.insert_chars(ps(params, 0, 1) as usize),\r\n            'P' => self.delete_chars(ps(params, 0, 1) as usize),\r\n            'L' => self.insert_lines(ps(params, 0, 1) as usize),\r\n            'M' => self.delete_lines(ps(params, 0, 1) as usize),\r\n            'S' => self.scroll_up(ps(params, 0, 1) as usize),\r","truncated":true,"truncatedBy":"lines","totalLines":525,"totalBytes":11057,"outputLines":285,"outputBytes":11057,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"impl GridState {\n…\n    fn switch_alt(&mut self, enter: bool, save_restore: bool) {\n…\n        if enter {\n…\n            self.pending_wrap = false;\r\n        } else {\r\n            self.alt_active = false;\r\n            if save_restore {\r\n                if let Some((r, c)) = self.saved.take() {\r\n                    self.row = r;\r\n                    self.col = c;\r\n                }\r\n            }\r\n            self.pending_wrap = false;\r\n            self.clamp_cursor();\r\n        }\r\n    }\r\n\r\n    fn apply_sgr(&mut self, params: &Params) {\r\n        let items: Vec<&[u16]> = params.iter().collect();\r\n        if items.is_empty() {\r\n            self.pen = Pen::default();\r\n            return;\r\n        }\r\n        let mut i = 0;\r\n        while i < items.len() {\r\n            let item = items[i];\r\n            let code = item.first().copied().unwrap_or(0);\r\n            match code {\r\n                0 => self.pen = Pen::default(),\r\n                1 => self.pen.bold = true,\r\n                2 => self.pen.dim = true,\r\n                3 => self.pen.italic = true,\r\n                4 => self.pen.underline = true,\r\n                5 => self.pen.blink = true,\r\n                7 => self.pen.reverse = true,\r\n                8 => self.pen.hidden = true,\r\n                9 => self.pen.strike = true,\r\n                22 => {\r\n                    self.pen.bold = false;\r\n                    self.pen.dim = false;\r\n                }\r\n                23 => self.pen.italic = false,\r\n                24 => self.pen.underline = false,\r\n                25 => self.pen.blink = false,\r\n                27 => self.pen.reverse = false,\r\n                28 => self.pen.hidden = false,\r\n                29 => self.pen.strike = false,\r\n                30..=37 => self.pen.fg = Color::Idx((code - 30) as u8),\r\n                39 => self.pen.fg = Color::Default,\r\n                40..=47 => self.pen.bg = Color::Idx((code - 40) as u8),\r\n                49 => self.pen.bg = Color::Default,\r\n                90..=97 => self.pen.fg = Color::Idx((code - 90 + 8) as u8),\r\n                100..=107 => self.pen.bg = Color::Idx((code - 100 + 8) as u8),\r\n                38 => self.pen.fg = extended_color(&items, &mut i).unwrap_or(self.pen.fg),\r\n                48 => self.pen.bg = extended_color(&items, &mut i).unwrap_or(self.pen.bg),\r\n                _ => {}\r\n            }\r\n            i += 1;\r\n        }\r\n    }\r\n\r\n    fn resize(&mut self, rows: u16, cols: u16) {\r\n        let rows = rows.max(1) as usize;\r\n        let cols = cols.max(1) as usize;\r\n        self.main = resized_buffer(&self.main, rows, cols);\r\n        self.alt = resized_buffer(&self.alt, rows, cols);\r\n        self.rows = rows;\r\n        self.cols = cols;\r\n        self.scroll_top = 0;\r\n        self.scroll_bottom = rows - 1;\r\n        self.pending_wrap = false;\r\n        self.clamp_cursor();\r\n    }\r\n\r\n    fn full_reset(&mut self) {\r\n        let (rows, cols) = (self.rows, self.cols);\r\n        *self = GridState::new(rows as u16, cols as u16);\r\n    }\r\n\r\n    // --- Repaint ------------------------------------------------------------\r\n\r\n    fn render_repaint(&self) -> Vec<u8> {\r\n        let mut out = String::new();\r\n        // Put the client on the matching buffer so its own scrollback is never\r\n        // touched: 1049h saves+switches+clears the alt screen; 1049l returns to main.\r\n        if self.alt_active {\r\n            out.push_str(\"\\x1b[?1049h\");\r\n        } else {\r\n            out.push_str(\"\\x1b[?1049l\");\r\n        }\r\n        // Replay the child's last-set window title (OSC 0/2) so a cold attach\r\n        // shows the CURRENT title, not the stale pre-attach one (E-2). BEL\r\n        // terminator — the widest-compat form.\r\n        // [impl->REQ-BROKER-SCREEN-GRID]\r\n        if let Some(title) = &self.title {\r\n            out.push_str(&format!(\"\\x1b]2;{title}\\x07\"));\r\n        }\r\n        out.push_str(\"\\x1b[0m\"); // reset pen\r\n        out.push_str(\"\\x1b[2J\"); // clear the (now-active) screen\r\n        out.push_str(\"\\x1b[H\"); // home\r\n        let mut emitted = Pen::default();\r\n        let buf = self.buf();\r\n        for (r, line) in buf.iter().enumerate() {\r\n            // Trim trailing default-blank cells — the screen is already cleared,\r\n            // so only paint what carries content or a non-default background.\r\n            let last = line.iter().rposition(|c| !c.is_default_blank());\r\n            let Some(last) = last else { continue };\r\n            out.push_str(&format!(\"\\x1b[{};1H\", r + 1));\r\n            for cell in &line[..=last] {\r\n                if cell.pen != emitted {\r\n                    out.push_str(&cell.pen.encode());\r\n                    emitted = cell.pen;\r\n                }\r\n                out.push(cell.ch);\r\n            }\r\n        }\r\n        // Restore the child's live pen so the next raw frames render with the SGR\r\n        // state the child assumes is current.\r\n        out.push_str(&self.pen.encode());\r\n        out.push_str(if self.cursor_visible {\r\n            \"\\x1b[?25h\"\r\n        } else {\r\n            \"\\x1b[?25l\"\r\n        });\r\n        // Cursor last, at its live position.\r\n        out.push_str(&format!(\"\\x1b[{};{}H\", self.row + 1, self.col + 1));\r\n        out.into_bytes()\r\n    }\r\n}\r\n\r\n/// Parse a `38;5;n` / `38;2;r;g;b` (or the colon-subparam form `38:5:n` /\r\n/// `38:2:r:g:b`) extended colour, advancing the semicolon index past any params it\r\n/// consumes. Returns `None` on a malformed selector.\r\nfn extended_color(items: &[&[u16]], i: &mut usize) -> Option<Color> {\r\n    let item = items[*i];\r\n    if item.len() >= 2 {\r\n        // Colon-subparam form: everything is in this one param slice.\r\n        match item[1] {\r\n            5 => item.get(2).map(|&n| Color::Idx(n as u8)),\r\n            2 => {\r\n                // 38:2:r:g:b, or 38:2:cs:r:g:b — take the last three components.\r\n                let n = item.len();\r\n                if n >= 5 {\r\n                    Some(Color::Rgb(\r\n                        item[n - 3] as u8,\r\n                        item[n - 2] as u8,\r\n                        item[n - 1] as u8,\r\n                    ))\r\n                } else {\r\n                    None\r\n                }\r\n            }\r\n            _ => None,\r\n        }\r\n    } else {\r\n        // Semicolon form: read the mode + components from the following params.\r\n        let mode = items.get(*i + 1).and_then(|p| p.first()).copied();\r\n        match mode {\r\n            Some(5) => {\r\n                let n = items.get(*i + 2).and_then(|p| p.first()).copied();\r\n                *i += 2;\r\n                n.map(|n| Color::Idx(n as u8))\r\n            }\r\n            Some(2) => {\r\n                let r = items.get(*i + 2).and_then(|p| p.first()).copied();\r\n                let g = items.get(*i + 3).and_then(|p| p.first()).copied();\r\n                let b = items.get(*i + 4).and_then(|p| p.first()).copied();\r\n                *i += 4;\r\n                match (r, g, b) {\r\n                    (Some(r), Some(g), Some(b)) => Some(Color::Rgb(r as u8, g as u8, b as u8)),\r\n                    _ => None,\r\n                }\r\n            }\r\n            _ => None,\r\n        }\r\n    }\r\n}\r\n\r\nfn blank_buffer(rows: usize, cols: usize) -> Vec<Vec<Cell>> {\r\n    vec![vec![Cell::default(); cols]; rows]\r\n}\r\n\r\n/// Reallocate a buffer to `rows × cols`, copying the overlapping top-left region.\r\nfn resized_buffer(old: &[Vec<Cell>], rows: usize, cols: usize) -> Vec<Vec<Cell>> {\r\n    let mut buf = blank_buffer(rows, cols);\r\n    for (r, line) in old.iter().take(rows).enumerate() {\r\n        for (c, cell) in line.iter().take(cols).enumerate() {\r\n            buf[r][c] = *cell;\r\n        }\r\n    }\r\n    buf\r\n}\r\n\r\n/// First sub-value of the `n`th CSI param, or `default` when absent/zero-omitted.\r\nfn ps(params: &Params, n: usize, default: u16) -> u16 {\r\n    params\r\n        .iter()\r\n        .nth(n)\r\n        .and_then(|p| p.first())\r\n        .copied()\r\n        .filter(|&v| v != 0)\r\n        .unwrap_or(default)\r\n}\r\n\r\nimpl Perform for GridState {\r\n    fn print(&mut self, c: char) {\r\n        self.put_char(c);\r\n    }\r\n\r\n    fn execute(&mut self, byte: u8) {\r\n        match byte {\r\n            0x08 => {\r\n                // BS\r\n                self.pending_wrap = false;\r\n                if self.col > 0 {\r\n                    self.col -= 1;\r\n                }\r\n            }\r\n            0x09 => {\r\n                // HT — next multiple of 8, clamped\r\n                self.pending_wrap = false;\r\n                let next = ((self.col / 8) + 1) * 8;\r\n                self.col = next.min(self.cols - 1);\r\n            }\r\n            0x0A..=0x0C => {\r\n                // LF / VT / FF\r\n                self.pending_wrap = false;\r\n                self.line_feed();\r\n            }\r\n            0x0D => {\r\n                // CR\r\n                self.pending_wrap = false;\r\n                self.col = 0;\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\r\n        let private = intermediates.first() == Some(&b'?');\r\n        match action {\r\n            'A' => {\r\n                self.pending_wrap = false;\r\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\r\n            }\r\n            'B' | 'e' => {\r\n                self.pending_wrap = false;\r\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\r\n            }\r\n            'C' | 'a' => {\r\n                self.pending_wrap = false;\r\n                self.col = (self.col + ps(params, 0, 1) as usize).min(self.cols - 1);\r\n            }\r\n            'D' => {\r\n                self.pending_wrap = false;\r\n                self.col = self.col.saturating_sub(ps(params, 0, 1) as usize);\r\n            }\r\n            'E' => {\r\n                self.pending_wrap = false;\r\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\r\n                self.col = 0;\r\n            }\r\n            'F' => {\r\n                self.pending_wrap = false;\r\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\r\n                self.col = 0;\r\n            }\r\n            'G' | '`' => {\r\n                self.pending_wrap = false;\r\n                self.col = (ps(params, 0, 1) as usize - 1).min(self.cols - 1);\r\n            }\r\n            'd' => {\r\n                self.pending_wrap = false;\r\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\r\n            }\r\n            'H' | 'f' => {\r\n                self.pending_wrap = false;\r\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\r\n                self.col = (ps(params, 1, 1) as usize - 1).min(self.cols - 1);\r\n            }\r\n            'J' => self.erase_display(ps(params, 0, 0)),\r\n            'K' => self.erase_line(ps(params, 0, 0)),\r\n            'X' => self.erase_chars(ps(params, 0, 1) as usize),\r\n            '@' => self.insert_chars(ps(params, 0, 1) as usize),\r\n            'P' => self.delete_chars(ps(params, 0, 1) as usize),\r\n            'L' => self.insert_lines(ps(params, 0, 1) as usize),\r\n            'M' => self.delete_lines(ps(params, 0, 1) as usize),\r\n            'S' => self.scroll_up(ps(params, 0, 1) as usize),\r\n…\n        }\n    }\n…\n}","startLine":217,"lineNumbers":[217,null,483,null,491,null,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,null,801,802,null,857]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\screen.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1023,"totalBytes":11057,"outputLines":285,"outputBytes":11057,"shownRange":{"start":499,"end":783},"nextOffset":784}}},"isError":false,"timestamp":1784365494707}}
{"type":"message","id":"55a77fdd","parentId":"ba023f8d","timestamp":"2026-07-18T09:04:54.720Z","message":{"role":"toolResult","toolCallId":"call_ZtyT5tQlkQRxxYUylL7zRJTN|fc_0ce5441d29b9f05d016a5b41b69c40819bbf32d65069a8c4c6","toolName":"read","content":[{"type":"text","text":"impl Perform for GridState {\n…\n    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\n…\n        match action {\n…\n            '@' => self.insert_chars(ps(params, 0, 1) as usize),\n            'P' => self.delete_chars(ps(params, 0, 1) as usize),\n            'L' => self.insert_lines(ps(params, 0, 1) as usize),\n            'M' => self.delete_lines(ps(params, 0, 1) as usize),\n            'S' => self.scroll_up(ps(params, 0, 1) as usize),\n            'T' => self.scroll_down(ps(params, 0, 1) as usize),\n            'm' => self.apply_sgr(params),\n            'h' => self.set_mode(private, ps(params, 0, 0), true),\n            'l' => self.set_mode(private, ps(params, 0, 0), false),\n            'r' if !private => {\n                // DECSTBM — set the scroll region (1-based, inclusive).\n                let top = ps(params, 0, 1) as usize - 1;\n                let bottom = ps(params, 1, self.rows as u16) as usize - 1;\n                if top < bottom && bottom < self.rows {\n                    self.scroll_top = top;\n                    self.scroll_bottom = bottom;\n                    self.row = top;\n                    self.col = 0;\n                    self.pending_wrap = false;\n                }\n            }\n            _ => {}\n        }\n    }\n\n    // OSC 0 (icon+title) / OSC 2 (title): capture the child's window title so\n    // the repaint can replay it — vte's default drops OSC entirely, which is\n    // exactly the E-2 stale-title-on-cold-attach hole. Other OSC codes stay\n    // ignored (colour palette etc. are client-local).\n    // [impl->REQ-BROKER-SCREEN-GRID]\n    fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {\n        if let [code, rest @ ..] = params {\n            if code == b\"0\" || code == b\"2\" {\n                let title = rest\n                    .iter()\n                    .map(|p| String::from_utf8_lossy(p))\n                    .collect::<Vec<_>>()\n                    .join(\";\");\n                self.title = Some(title);\n            }\n        }\n    }\n\n    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {\n        // Ignore charset-designation escapes (intermediates present, e.g. ESC ( B).\n        if !intermediates.is_empty() {\n            return;\n        }\n        match byte {\n            b'c' => self.full_reset(),                 // RIS\n            b'7' => self.saved = Some((self.row, self.col)), // DECSC\n            b'8' => {\n                // DECRC\n                if let Some((r, c)) = self.saved {\n                    self.row = r.min(self.rows - 1);\n                    self.col = c.min(self.cols - 1);\n                    self.pending_wrap = false;\n                }\n            }\n            b'D' => {\n                // IND — index (down + scroll)\n                self.pending_wrap = false;\n                self.line_feed();\n            }\n            b'M' => {\n                // RI — reverse index (up + scroll)\n                self.pending_wrap = false;\n                self.reverse_line_feed();\n            }\n            b'E' => {\n                // NEL — next line\n                self.pending_wrap = false;\n                self.col = 0;\n                self.line_feed();\n            }\n            _ => {}\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn repaint(feed: &[&[u8]], rows: u16, cols: u16) -> String {\n        let mut g = ScreenGrid::new(rows, cols);\n        for chunk in feed {\n            g.advance(chunk);\n        }\n        String::from_utf8(g.render_repaint()).unwrap()\n    }\n\n    // A main-screen repaint carries the visible text, keeps the client on the main\n    // buffer (?1049l), and never enters the alt screen.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn main_screen_repaint_paints_text_no_alt_enter() {\n        let out = repaint(&[b\"hello world\"], 24, 80);\n        assert!(out.contains(\"hello world\"), \"repaint paints the text: {out:?}\");\n        assert!(out.contains(\"\\x1b[?1049l\"), \"client stays on main buffer\");\n        assert!(!out.contains(\"\\x1b[?1049h\"), \"no alt-screen enter for a main screen\");\n        // Cursor ends after the 11 printed columns (col 12, 1-based).\n        assert!(out.contains(\"\\x1b[1;12H\"), \"cursor at live position: {out:?}\");\n    }\n\n    // The #6 CORE: after entering the alt screen, the repaint paints ONLY the\n    // alt-screen content and does NOT dump the main-screen scrollback written\n    // before the switch.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn alt_screen_repaint_excludes_main_scrollback() {\n        let out = repaint(\n            &[\n                b\"MAIN-SCROLLBACK-LINE\\r\\n\",     // main screen history\n                b\"\\x1b[?1049h\",                   // enter alt screen\n                b\"\\x1b[2J\\x1b[HTUI-VIEWPORT\",     // paint the TUI\n            ],\n            24,\n            80,\n        );\n        assert!(out.contains(\"\\x1b[?1049h\"), \"repaint puts the client in the alt screen\");\n        assert!(out.contains(\"TUI-VIEWPORT\"), \"alt content is painted: {out:?}\");\n        assert!(\n            !out.contains(\"MAIN-SCROLLBACK-LINE\"),\n            \"main scrollback must NOT appear in an alt repaint (the #6 corruption): {out:?}\"\n        );\n    }\n\n    // Leaving the alt screen restores the main buffer + its content, still no\n    // stray alt residue.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn leaving_alt_restores_main_screen() {\n        let out = repaint(\n            &[\n                b\"back-on-main\",\n                b\"\\x1b[?1049h\\x1b[2J\\x1b[Halt-only\",\n                b\"\\x1b[?1049l\", // leave alt\n            ],\n            24,\n            80,\n        );\n        assert!(out.contains(\"\\x1b[?1049l\"), \"client returned to main\");\n        assert!(out.contains(\"back-on-main\"), \"main content restored: {out:?}\");\n        assert!(!out.contains(\"alt-only\"), \"alt content gone after leaving: {out:?}\");\n    }\n\n    // SGR pen state is reconstructed in the repaint (colour + bold), and the reset\n    // pen closes it.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn sgr_pen_reconstructed_in_repaint() {\n        let out = repaint(&[b\"\\x1b[1;31mRED\\x1b[0m plain\"], 24, 80);\n        assert!(out.contains(\"RED\"), \"text painted\");\n        assert!(out.contains(\"\\x1b[0;1;31m\"), \"bold-red SGR reconstructed: {out:?}\");\n        assert!(out.contains(\"plain\"), \"trailing plain text painted\");\n    }\n\n    // E-2 red-first: the child's OSC title is captured and REPLAYED in the\n    // repaint alongside the content — a cold attach shows the current title,\n    // never the attacher's stale one (vte's default drops OSC, so pre-fix the\n    // repaint carried no title at all).\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn title_captured_and_replayed_in_repaint() {\n        let out = repaint(&[b\"\\x1b]2;my-session\\x07hello\"], 24, 80);\n        assert!(out.contains(\"hello\"), \"text painted: {out:?}\");\n        assert!(\n            out.contains(\"\\x1b]2;my-session\\x07\"),\n            \"the last-set title is replayed: {out:?}\"\n        );\n        // OSC 0 (icon+title) captures too, and an embedded ';' survives the\n        // param rejoin.\n        let out = repaint(&[b\"\\x1b]0;left;right\\x07\"], 24, 80);\n        assert!(\n            out.contains(\"\\x1b]2;left;right\\x07\"),\n            \"OSC 0 sets the title, semicolons preserved: {out:?}\"\n        );\n        // A later title supersedes an earlier one.\n        let out = repaint(&[b\"\\x1b]2;old\\x07\\x1b]2;new\\x07\"], 24, 80);\n        assert!(out.contains(\"\\x1b]2;new\\x07\") && !out.contains(\"\\x1b]2;old\\x07\"));\n    }\n\n    // E-2: RIS is a full reset — the captured title dies with the rest of the\n    // state (no stale title replayed after the child hard-resets); an untitled\n    // grid emits no OSC at all.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn ris_clears_title_and_untitled_emits_none() {\n        let out = repaint(&[b\"\\x1b]2;doomed\\x07\", b\"\\x1bc\"], 24, 80);\n        assert!(!out.contains(\"\\x1b]2;\"), \"RIS cleared the title: {out:?}\");\n        let out = repaint(&[b\"plain\"], 24, 80);\n        assert!(!out.contains(\"\\x1b]2;\"), \"no title ever set → none replayed\");\n    }\n\n    // E-2: a resize does not drop the title (only buffers reflow).\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn resize_preserves_title() {\n        let mut g = ScreenGrid::new(24, 80);\n        g.advance(b\"\\x1b]2;sticky\\x07\");\n        g.resize(10, 40);\n        let out = String::from_utf8(g.render_repaint()).unwrap();\n        assert!(out.contains(\"\\x1b]2;sticky\\x07\"), \"title survives resize: {out:?}\");\n    }\n\n    // A resize preserves the visible top-left content.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn resize_preserves_content() {\n        let mut g = ScreenGrid::new(24, 80);\n        g.advance(b\"keep-me\");\n        g.resize(10, 40);\n        let out = String::from_utf8(g.render_repaint()).unwrap();\n        assert!(out.contains(\"keep-me\"), \"content survives resize: {out:?}\");\n    }\n\n    // Erase-display (ED 2) clears everything painted before it.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn erase_display_clears_screen() {\n        let out = repaint(&[b\"garbage\\x1b[2J\\x1b[Hclean\"], 24, 80);\n        assert!(!out.contains(\"garbage\"), \"cleared text is gone: {out:?}\");\n        assert!(out.contains(\"clean\"), \"post-clear text remains\");\n    }\n\n    // Deferred auto-wrap: a full row then one more char lands on the next row, not\n    // a phantom blank line.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn cursor_visibility_hidden_emitted() {\n        let out = repaint(&[b\"\\x1b[?25l\"], 24, 80);\n        assert!(out.contains(\"\\x1b[?25l\"), \"hidden cursor reflected: {out:?}\");\n    }\n\n    // An empty grid (nothing produced) repaints to a clean clear with no cell rows.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn empty_grid_repaint_is_clean() {\n        let out = repaint(&[], 24, 80);\n        assert!(out.contains(\"\\x1b[2J\"), \"empty repaint still clears\");\n        assert!(out.contains(\"\\x1b[1;1H\"), \"cursor homed\");\n    }\n}\n"}],"details":{"displayContent":{"text":"impl Perform for GridState {\n…\n    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\n…\n        match action {\n…\n            '@' => self.insert_chars(ps(params, 0, 1) as usize),\r\n            'P' => self.delete_chars(ps(params, 0, 1) as usize),\r\n            'L' => self.insert_lines(ps(params, 0, 1) as usize),\r\n            'M' => self.delete_lines(ps(params, 0, 1) as usize),\r\n            'S' => self.scroll_up(ps(params, 0, 1) as usize),\r\n            'T' => self.scroll_down(ps(params, 0, 1) as usize),\r\n            'm' => self.apply_sgr(params),\r\n            'h' => self.set_mode(private, ps(params, 0, 0), true),\r\n            'l' => self.set_mode(private, ps(params, 0, 0), false),\r\n            'r' if !private => {\r\n                // DECSTBM — set the scroll region (1-based, inclusive).\r\n                let top = ps(params, 0, 1) as usize - 1;\r\n                let bottom = ps(params, 1, self.rows as u16) as usize - 1;\r\n                if top < bottom && bottom < self.rows {\r\n                    self.scroll_top = top;\r\n                    self.scroll_bottom = bottom;\r\n                    self.row = top;\r\n                    self.col = 0;\r\n                    self.pending_wrap = false;\r\n                }\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    // OSC 0 (icon+title) / OSC 2 (title): capture the child's window title so\r\n    // the repaint can replay it — vte's default drops OSC entirely, which is\r\n    // exactly the E-2 stale-title-on-cold-attach hole. Other OSC codes stay\r\n    // ignored (colour palette etc. are client-local).\r\n    // [impl->REQ-BROKER-SCREEN-GRID]\r\n    fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {\r\n        if let [code, rest @ ..] = params {\r\n            if code == b\"0\" || code == b\"2\" {\r\n                let title = rest\r\n                    .iter()\r\n                    .map(|p| String::from_utf8_lossy(p))\r\n                    .collect::<Vec<_>>()\r\n                    .join(\";\");\r\n                self.title = Some(title);\r\n            }\r\n        }\r\n    }\r\n\r\n    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {\r\n        // Ignore charset-designation escapes (intermediates present, e.g. ESC ( B).\r\n        if !intermediates.is_empty() {\r\n            return;\r\n        }\r\n        match byte {\r\n            b'c' => self.full_reset(),                 // RIS\r\n            b'7' => self.saved = Some((self.row, self.col)), // DECSC\r\n            b'8' => {\r\n                // DECRC\r\n                if let Some((r, c)) = self.saved {\r\n                    self.row = r.min(self.rows - 1);\r\n                    self.col = c.min(self.cols - 1);\r\n                    self.pending_wrap = false;\r\n                }\r\n            }\r\n            b'D' => {\r\n                // IND — index (down + scroll)\r\n                self.pending_wrap = false;\r\n                self.line_feed();\r\n            }\r\n            b'M' => {\r\n                // RI — reverse index (up + scroll)\r\n                self.pending_wrap = false;\r\n                self.reverse_line_feed();\r\n            }\r\n            b'E' => {\r\n                // NEL — next line\r\n                self.pending_wrap = false;\r\n                self.col = 0;\r\n                self.line_feed();\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    fn repaint(feed: &[&[u8]], rows: u16, cols: u16) -> String {\r\n        let mut g = ScreenGrid::new(rows, cols);\r\n        for chunk in feed {\r\n            g.advance(chunk);\r\n        }\r\n        String::from_utf8(g.render_repaint()).unwrap()\r\n    }\r\n\r\n    // A main-screen repaint carries the visible text, keeps the client on the main\r\n    // buffer (?1049l), and never enters the alt screen.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn main_screen_repaint_paints_text_no_alt_enter() {\r\n        let out = repaint(&[b\"hello world\"], 24, 80);\r\n        assert!(out.contains(\"hello world\"), \"repaint paints the text: {out:?}\");\r\n        assert!(out.contains(\"\\x1b[?1049l\"), \"client stays on main buffer\");\r\n        assert!(!out.contains(\"\\x1b[?1049h\"), \"no alt-screen enter for a main screen\");\r\n        // Cursor ends after the 11 printed columns (col 12, 1-based).\r\n        assert!(out.contains(\"\\x1b[1;12H\"), \"cursor at live position: {out:?}\");\r\n    }\r\n\r\n    // The #6 CORE: after entering the alt screen, the repaint paints ONLY the\r\n    // alt-screen content and does NOT dump the main-screen scrollback written\r\n    // before the switch.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn alt_screen_repaint_excludes_main_scrollback() {\r\n        let out = repaint(\r\n            &[\r\n                b\"MAIN-SCROLLBACK-LINE\\r\\n\",     // main screen history\r\n                b\"\\x1b[?1049h\",                   // enter alt screen\r\n                b\"\\x1b[2J\\x1b[HTUI-VIEWPORT\",     // paint the TUI\r\n            ],\r\n            24,\r\n            80,\r\n        );\r\n        assert!(out.contains(\"\\x1b[?1049h\"), \"repaint puts the client in the alt screen\");\r\n        assert!(out.contains(\"TUI-VIEWPORT\"), \"alt content is painted: {out:?}\");\r\n        assert!(\r\n            !out.contains(\"MAIN-SCROLLBACK-LINE\"),\r\n            \"main scrollback must NOT appear in an alt repaint (the #6 corruption): {out:?}\"\r\n        );\r\n    }\r\n\r\n    // Leaving the alt screen restores the main buffer + its content, still no\r\n    // stray alt residue.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn leaving_alt_restores_main_screen() {\r\n        let out = repaint(\r\n            &[\r\n                b\"back-on-main\",\r\n                b\"\\x1b[?1049h\\x1b[2J\\x1b[Halt-only\",\r\n                b\"\\x1b[?1049l\", // leave alt\r\n            ],\r\n            24,\r\n            80,\r\n        );\r\n        assert!(out.contains(\"\\x1b[?1049l\"), \"client returned to main\");\r\n        assert!(out.contains(\"back-on-main\"), \"main content restored: {out:?}\");\r\n        assert!(!out.contains(\"alt-only\"), \"alt content gone after leaving: {out:?}\");\r\n    }\r\n\r\n    // SGR pen state is reconstructed in the repaint (colour + bold), and the reset\r\n    // pen closes it.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn sgr_pen_reconstructed_in_repaint() {\r\n        let out = repaint(&[b\"\\x1b[1;31mRED\\x1b[0m plain\"], 24, 80);\r\n        assert!(out.contains(\"RED\"), \"text painted\");\r\n        assert!(out.contains(\"\\x1b[0;1;31m\"), \"bold-red SGR reconstructed: {out:?}\");\r\n        assert!(out.contains(\"plain\"), \"trailing plain text painted\");\r\n    }\r\n\r\n    // E-2 red-first: the child's OSC title is captured and REPLAYED in the\r\n    // repaint alongside the content — a cold attach shows the current title,\r\n    // never the attacher's stale one (vte's default drops OSC, so pre-fix the\r\n    // repaint carried no title at all).\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn title_captured_and_replayed_in_repaint() {\r\n        let out = repaint(&[b\"\\x1b]2;my-session\\x07hello\"], 24, 80);\r\n        assert!(out.contains(\"hello\"), \"text painted: {out:?}\");\r\n        assert!(\r\n            out.contains(\"\\x1b]2;my-session\\x07\"),\r\n            \"the last-set title is replayed: {out:?}\"\r\n        );\r\n        // OSC 0 (icon+title) captures too, and an embedded ';' survives the\r\n        // param rejoin.\r\n        let out = repaint(&[b\"\\x1b]0;left;right\\x07\"], 24, 80);\r\n        assert!(\r\n            out.contains(\"\\x1b]2;left;right\\x07\"),\r\n            \"OSC 0 sets the title, semicolons preserved: {out:?}\"\r\n        );\r\n        // A later title supersedes an earlier one.\r\n        let out = repaint(&[b\"\\x1b]2;old\\x07\\x1b]2;new\\x07\"], 24, 80);\r\n        assert!(out.contains(\"\\x1b]2;new\\x07\") && !out.contains(\"\\x1b]2;old\\x07\"));\r\n    }\r\n\r\n    // E-2: RIS is a full reset — the captured title dies with the rest of the\r\n    // state (no stale title replayed after the child hard-resets); an untitled\r\n    // grid emits no OSC at all.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn ris_clears_title_and_untitled_emits_none() {\r\n        let out = repaint(&[b\"\\x1b]2;doomed\\x07\", b\"\\x1bc\"], 24, 80);\r\n        assert!(!out.contains(\"\\x1b]2;\"), \"RIS cleared the title: {out:?}\");\r\n        let out = repaint(&[b\"plain\"], 24, 80);\r\n        assert!(!out.contains(\"\\x1b]2;\"), \"no title ever set → none replayed\");\r\n    }\r\n\r\n    // E-2: a resize does not drop the title (only buffers reflow).\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn resize_preserves_title() {\r\n        let mut g = ScreenGrid::new(24, 80);\r\n        g.advance(b\"\\x1b]2;sticky\\x07\");\r\n        g.resize(10, 40);\r\n        let out = String::from_utf8(g.render_repaint()).unwrap();\r\n        assert!(out.contains(\"\\x1b]2;sticky\\x07\"), \"title survives resize: {out:?}\");\r\n    }\r\n\r\n    // A resize preserves the visible top-left content.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn resize_preserves_content() {\r\n        let mut g = ScreenGrid::new(24, 80);\r\n        g.advance(b\"keep-me\");\r\n        g.resize(10, 40);\r\n        let out = String::from_utf8(g.render_repaint()).unwrap();\r\n        assert!(out.contains(\"keep-me\"), \"content survives resize: {out:?}\");\r\n    }\r\n\r\n    // Erase-display (ED 2) clears everything painted before it.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn erase_display_clears_screen() {\r\n        let out = repaint(&[b\"garbage\\x1b[2J\\x1b[Hclean\"], 24, 80);\r\n        assert!(!out.contains(\"garbage\"), \"cleared text is gone: {out:?}\");\r\n        assert!(out.contains(\"clean\"), \"post-clear text remains\");\r\n    }\r\n\r\n    // Deferred auto-wrap: a full row then one more char lands on the next row, not\r\n    // a phantom blank line.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn cursor_visibility_hidden_emitted() {\r\n        let out = repaint(&[b\"\\x1b[?25l\"], 24, 80);\r\n        assert!(out.contains(\"\\x1b[?25l\"), \"hidden cursor reflected: {out:?}\");\r\n    }\r\n\r\n    // An empty grid (nothing produced) repaints to a clean clear with no cell rows.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn empty_grid_repaint_is_clean() {\r\n        let out = repaint(&[], 24, 80);\r\n        assert!(out.contains(\"\\x1b[2J\"), \"empty repaint still clears\");\r\n        assert!(out.contains(\"\\x1b[1;1H\"), \"cursor homed\");\r\n    }\r\n}\r\n","startLine":700,"lineNumbers":[700,null,734,null,736,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\screen.rs"}}},"isError":false,"timestamp":1784365494720}}
{"type":"message","id":"6abfb760","parentId":"55a77fdd","timestamp":"2026-07-18T09:04:54.735Z","message":{"role":"toolResult","toolCallId":"call_PhDHrlm4TDMw29kRNgdUaQZ4|fc_0ce5441d29b9f05d016a5b41b69c54819bad69f04abd5f4fc6","toolName":"read","content":[{"type":"text","text":"/// is being attached, for the gate. `net_from_seq` is this brain's resume\n/// cursor on the attach stream (0 cold — also the safe post-restart choice:\n/// the replayed `Request`/`Input` records re-gate and dedup at the PTY\n/// journal, and re-pumped output dedups at the operator's render cursor).\n///\n/// `remote_session` is `Some(sid)` ONLY on the cross-node leg (#4,\n/// REQ-RC-CROSS-NODE-ATTACH): the dispatcher has ALREADY resolved endpoint→our\n/// session id ([`resolve_local_session`]) BEFORE this serve loop subscribed, so\n/// the wire's placeholder `session_id` (0) is overridden here with the resolved\n/// id. It is passed down — never re-queried mid-loop — because a `sessions()`\n/// round-trip inside the event loop would swallow interleaved attach records\n/// (`Input`/`Subscribed`) the loop must see. `None` = the local path, which uses\n/// the wire `session_id`.\n///\n/// `seed_request` is the broker-pinned opener Request (ADR-0038 B): after the\n/// bounded ring rolls, the wire replay no longer carries the stream's Request\n/// record — a reconstruction that waited for one would idle forever. The\n/// dispatcher hands the durable opener here and it is handled EXACTLY like a\n/// wire-arriving Request (same gate, same subscribe); a Request that then\n/// replays from the ring anyway re-runs the same-identity re-take, which is\n/// silent and idempotent (the successor-re-take parity the fix must keep).\n// [impl->REQ-INST-8]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\n// [impl->REQ-STREAM-OPENER-DURABLE]\n#[allow(clippy::too_many_arguments)]\npub fn serve_attach(\n    brain: &mut Brain,\n    broker_name: &str,\n    stream_id: u64,\n    endpoint: &str,\n    origin_node: &str,\n    net_from_seq: u64,\n    remote_session: Option<u64>,\n    seed_request: Option<AttachRecord>,\n) -> io::Result<AttachServeOutcome> {\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\n    // session output (broker→serve) — including the controller's synchronous,\n    // inline, multi-frame ring replay (`become_controller`), which holds the\n    // broker's single-threaded per-conn handler for the whole replay. If the\n    // forwards rode `brain` too, that stuck handler could not read them →\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\n    // two directions on independent single-threaded handlers cannot block each\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\n    // a second conn is correct. Order is preserved: this one serve thread reads\n    // every Output in seq order on `brain` and forwards it in that same order on\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\n    // the controller onto that async path would remove the inline-replay block at\n    // the source, but it touches the authoritative cursor-advancing path.)\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\n    let mut decoder = AttachDecoder::new();\n    let mut attached = false;\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\n    // race on the input gate).\n    let mut intent = AttachIntent::Control;\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\n    // controller input that races ahead of the reply is BUFFERED, not dropped\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\n    // we know we control the session (the busy-refuse case).\n    let mut role: Option<ServeRole> = None;\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\n    // The attached session — captured from the Request so the terminal paths can\n    // explicitly release this worker's role (clearing driven_by / viewer_count\n    // promptly, not only on connection drop).\n    let mut attached_session: Option<u64> = None;\n    // This worker's attach GENERATION (ADR-0038 Amendment fix 6) — set when\n    // its Request lands; every terminal release carries it so a STALE worker\n    // (an older generation) can never clear a replacement controller.\n    let mut serve_gen: u64 = 0;\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\n    // intermittent live bursts, never a CPU spin.\n    let mut last_resubscribe: Option<Instant> = None;\n\n    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\n    // BEFORE the event loop, through the exact wire-Request path — a rolled\n    // ring can no longer replay it. Non-Request seeds are ignored (the opener\n    // of an attach stream is always its Request).\n    if let Some(AttachRecord::Request {\n        session_id,\n        from_seq,\n        intent: req_intent,\n        endpoint_id: _,\n        gen: req_gen,\n    }) = seed_request\n    {\n        if let Some(out) = handle_attach_request(\n            brain,\n            &mut wire,\n            stream_id,\n            endpoint,\n            origin_node,\n            remote_session,\n            session_id,\n            from_seq,\n            req_intent,\n            req_gen,\n            &mut intent,\n            &mut attached_session,\n            &mut attached,\n            &mut serve_gen,\n        )? {\n            return Ok(out);\n        }\n    }\n\n    loop {\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\n        // role resolves no Output flows, so a plain read is correct then.\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n        let event = match (role, attached_session) {\n            (Some(ServeRole::Controller), Some(sid)) => {\n                match read_controller_event_resuming(brain, sid, serve_gen, Some(origin_node)) {\n                    Ok(ev) => ev,\n                    Err(e)\n                        if e.get_ref()\n                            .is_some_and(|inner| inner.is::<ControllerIrrecoverablyBehind>()) =>\n                    {\n                        // Irrecoverable: the dropped frames rolled out of the ring.\n                        // SURFACE a marked truncation to the operator (never a\n                        // silent skip = B2 lie, never a spin), then end cleanly.\n                        // Graceful continuation is deferred\n                        // (REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND).\n                        let notice =\n                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\n                        let line = ndjson::encode_line(&AttachRecord::Output {\n                            seq: brain.controller_resume_floor(),\n                            data_b64: encode_bytes(notice),\n                        });\n                        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                            gen: req_gen,\n                        } => {\n                            if let Some(out) = handle_attach_request(\n                                brain,\n                                &mut wire,\n                                stream_id,\n                                endpoint,\n                                origin_node,\n                                remote_session,\n                                session_id,\n                                from_seq,\n                                req_intent,\n                                req_gen,\n                                &mut intent,\n                                &mut attached_session,\n                                &mut attached,\n                                &mut serve_gen,\n                            )? {\n                                return Ok(out);\n                            }\n                        }\n                        AttachRecord::Input { data_b64, op_id }\n                            if attached && intent != AttachIntent::Viewer =>\n                        {\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                            match role {\n                                // Confirmed controller — exactly-once at the PTY\n                                // (the operator-minted id rides the B5 journal). NO-ACK\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\n                                // fire-and-forward (the ack is discarded here) and can\n                                // flood N input frames on this one conn; an acked write\n                                // back would fill the return direction and deadlock the\n                                // broker's per-conn handler. Dedup still holds at the\n                                // broker (op_id applied-set).\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\n                                Some(ServeRole::Controller) => {\n                                    // The op_id is operator-minted (rc attach OpMinter),\n                                    // so it carries the `rc` journal tag (ADR-0034).\n                                    brain.send_effect_no_ack(\n                                        MintedOp::new(Minter::Rc, op_id),\n                                        &bytes,\n                                    )?\n                                }\n                                // Outcome not yet known — buffer; flushed on a\n                                // controller confirmation, discarded on busy/viewer.\n                                None => pending_input.push((op_id, bytes)),\n                                // A viewer never reaches here (intent gate).\n                                Some(ServeRole::Viewer) => {}\n                            }\n                        }\n                        AttachRecord::Resize { rows, cols }\n                            if attached && intent != AttachIntent::Viewer =>\n                        {\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\n                            // broker rejects it on a viewer's stream). A viewer's\n                            // Resize is dropped here by the intent gate too.\n                            let _ = brain.resize(rows, cols);\n                        }\n                        // Input/Resize before Request, a viewer's input/resize,\n                        // or target-direction records echoed back: noise — ignore.\n                        _ => {}\n                    }\n                }\n            }\n\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\n            BrokerEvent::Subscribed { outcome, .. } => {\n                use crate::msg::SubscribeOutcome as O;\n                match outcome {\n                    O::Controller | O::TookControl => {\n                        role = Some(ServeRole::Controller);\n                        // A driver attached — the wake edge (controller-only;\n                        // idempotent on an already-active seat).\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\n                        // Flush any input that raced ahead of the reply (operator-\n                        // minted ops carry the `rc` tag).\n                        for (op, b) in pending_input.drain(..) {\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\n                        }\n                    }\n                    O::Viewer => {\n                        role = Some(ServeRole::Viewer);\n                        // Wake-neutral: a viewer never wakes the endpoint, never\n                        // keeps it awake (Gap B). Discard any racing input.\n                        pending_input.clear();\n                    }\n                    O::BusyControlled { .. } => {\n                        // A different operator controls it — refuse (no PTY input\n                        // ever reached it; the buffer is discarded). The rc\n                        // renders the busy guidance client-side from driven_by.\n                        wire.net_stream_send(stream_id, &[], None, true)?;\n                        return Ok(AttachServeOutcome::Refused);\n                    }\n                }\n            }\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\n            BrokerEvent::Size { rows, cols, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\n                wire.net_stream_send(stream_id, &line, None, false)?;\n            }\n            // This controller was kicked by a different operator's `--take`.\n            BrokerEvent::Displaced { by, .. } if attached => {\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\n                wire.net_stream_send(stream_id, &line, None, true)?;\n                // No dormant edge: the endpoint stays driven by the taker (whose\n                // own serve loop already fired the wake).\n                return Ok(AttachServeOutcome::Displaced);\n            }\n\n            // This VIEWER overflowed its broker subscription queue and was evicted\n            // (it forwards slower than the drain fans out under flood). SKIP TO\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\n            // spin. The next live Output carries a forward seq jump; the operator's\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n            BrokerEvent::ViewerEvicted { session_id: sid }\n                if attached && Some(sid) == attached_session =>\n            {\n                if let Some(prev) = last_resubscribe {\n                    let since = prev.elapsed();\n                    if since < RESUBSCRIBE_INTERVAL {\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\n                    }\n                }\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\n                last_resubscribe = Some(Instant::now());\n            }\n\n            // The operator closed its send side: the viewport is over.\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                wire.net_stream_send(stream_id, &[], None, true)?;\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\n                // info.json, carrying the current driven_by through), THEN release\n                // the role over IPC so the broker's driven_by=None is the LAST\n                // write — otherwise a concurrent resting RMW restores the stale\n                // marker (the clear-race the broker-single-writer model averts).\n                if role == Some(ServeRole::Controller) {\n                    // The driver detached — the endpoint rests warm (controller-\n                    // only; a viewer leaving is wake-neutral).\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\n                }\n                if let Some(sid) = attached_session {\n…\n                }\n…\n            }\n…\n        }\n    }\n}\n\n[Showing lines 277-593 of 728. Use :594 to continue]"}],"details":{"truncation":{"content":"/// 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///\r\n/// `seed_request` is the broker-pinned opener Request (ADR-0038 B): after the\r\n/// bounded ring rolls, the wire replay no longer carries the stream's Request\r\n/// record — a reconstruction that waited for one would idle forever. The\r\n/// dispatcher hands the durable opener here and it is handled EXACTLY like a\r\n/// wire-arriving Request (same gate, same subscribe); a Request that then\r\n/// replays from the ring anyway re-runs the same-identity re-take, which is\r\n/// silent and idempotent (the successor-re-take parity the fix must keep).\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n// [impl->REQ-STREAM-OPENER-DURABLE]\r\n#[allow(clippy::too_many_arguments)]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n    seed_request: Option<AttachRecord>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // This worker's attach GENERATION (ADR-0038 Amendment fix 6) — set when\r\n    // its Request lands; every terminal release carries it so a STALE worker\r\n    // (an older generation) can never clear a replacement controller.\r\n    let mut serve_gen: u64 = 0;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\r\n    // BEFORE the event loop, through the exact wire-Request path — a rolled\r\n    // ring can no longer replay it. Non-Request seeds are ignored (the opener\r\n    // of an attach stream is always its Request).\r\n    if let Some(AttachRecord::Request {\r\n        session_id,\r\n        from_seq,\r\n        intent: req_intent,\r\n        endpoint_id: _,\r\n        gen: req_gen,\r\n    }) = seed_request\r\n    {\r\n        if let Some(out) = handle_attach_request(\r\n            brain,\r\n            &mut wire,\r\n            stream_id,\r\n            endpoint,\r\n            origin_node,\r\n            remote_session,\r\n            session_id,\r\n            from_seq,\r\n            req_intent,\r\n            req_gen,\r\n            &mut intent,\r\n            &mut attached_session,\r\n            &mut attached,\r\n            &mut serve_gen,\r\n        )? {\r\n            return Ok(out);\r\n        }\r\n    }\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, serve_gen, 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                            gen: req_gen,\r\n                        } => {\r\n                            if let Some(out) = handle_attach_request(\r\n                                brain,\r\n                                &mut wire,\r\n                                stream_id,\r\n                                endpoint,\r\n                                origin_node,\r\n                                remote_session,\r\n                                session_id,\r\n                                from_seq,\r\n                                req_intent,\r\n                                req_gen,\r\n                                &mut intent,\r\n                                &mut attached_session,\r\n                                &mut attached,\r\n                                &mut serve_gen,\r\n                            )? {\r\n                                return Ok(out);\r\n                            }\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n            BrokerEvent::Size { rows, cols, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            // This controller was kicked by a different operator's `--take`.\r\n            BrokerEvent::Displaced { by, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                // No dormant edge: the endpoint stays driven by the taker (whose\r\n                // own serve loop already fired the wake).\r\n                return Ok(AttachServeOutcome::Displaced);\r\n            }\r\n\r\n            // This VIEWER overflowed its broker subscription queue and was evicted\r\n            // (it forwards slower than the drain fans out under flood). SKIP TO\r\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\r\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\r\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\r\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\r\n            // spin. The next live Output carries a forward seq jump; the operator's\r\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\r\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n            BrokerEvent::ViewerEvicted { session_id: sid }\r\n                if attached && Some(sid) == attached_session =>\r\n            {\r\n                if let Some(prev) = last_resubscribe {\r\n                    let since = prev.elapsed();\r\n                    if since < RESUBSCRIBE_INTERVAL {\r\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\r\n                    }\r\n                }\r\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\r\n                last_resubscribe = Some(Instant::now());\r\n            }\r\n\r\n            // The operator closed its send side: the viewport is over.\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\r\n                // info.json, carrying the current driven_by through), THEN release\r\n                // the role over IPC so the broker's driven_by=None is the LAST\r\n                // write — otherwise a concurrent resting RMW restores the stale\r\n                // marker (the clear-race the broker-single-writer model averts).\r\n                if role == Some(ServeRole::Controller) {\r\n                    // The driver detached — the endpoint rests warm (controller-\r\n                    // only; a viewer leaving is wake-neutral).\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                if let Some(sid) = attached_session {\r","truncated":true,"truncatedBy":"lines","totalLines":452,"totalBytes":17923,"outputLines":317,"outputBytes":17923,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// 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///\r\n/// `seed_request` is the broker-pinned opener Request (ADR-0038 B): after the\r\n/// bounded ring rolls, the wire replay no longer carries the stream's Request\r\n/// record — a reconstruction that waited for one would idle forever. The\r\n/// dispatcher hands the durable opener here and it is handled EXACTLY like a\r\n/// wire-arriving Request (same gate, same subscribe); a Request that then\r\n/// replays from the ring anyway re-runs the same-identity re-take, which is\r\n/// silent and idempotent (the successor-re-take parity the fix must keep).\r\n// [impl->REQ-INST-8]\r\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\r\n// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\n// [impl->REQ-STREAM-OPENER-DURABLE]\r\n#[allow(clippy::too_many_arguments)]\r\npub fn serve_attach(\r\n    brain: &mut Brain,\r\n    broker_name: &str,\r\n    stream_id: u64,\r\n    endpoint: &str,\r\n    origin_node: &str,\r\n    net_from_seq: u64,\r\n    remote_session: Option<u64>,\r\n    seed_request: Option<AttachRecord>,\r\n) -> io::Result<AttachServeOutcome> {\r\n    brain.net_stream_subscribe(stream_id, net_from_seq)?;\r\n    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS\r\n    // (session Output / Size / Exit / Displaced / detach-finish onto the operator\r\n    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES\r\n    // session output (broker→serve) — including the controller's synchronous,\r\n    // inline, multi-frame ring replay (`become_controller`), which holds the\r\n    // broker's single-threaded per-conn handler for the whole replay. If the\r\n    // forwards rode `brain` too, that stuck handler could not read them →\r\n    // serve→broker backs up while broker→serve replay backs up → mutual IPC\r\n    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an\r\n    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The\r\n    // two directions on independent single-threaded handlers cannot block each\r\n    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from\r\n    // a second conn is correct. Order is preserved: this one serve thread reads\r\n    // every Output in seq order on `brain` and forwards it in that same order on\r\n    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a\r\n    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-\r\n    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying\r\n    // the controller onto that async path would remove the inline-replay block at\r\n    // the source, but it touches the authoritative cursor-advancing path.)\r\n    let mut wire = Brain::cold_start(broker_name, now_ms())?;\r\n    let mut decoder = AttachDecoder::new();\r\n    let mut attached = false;\r\n    // The Request's intent — gates wire input (a viewer's keystrokes never reach\r\n    // the PTY) independent of the broker's later role reply (no IPC-ordering\r\n    // race on the input gate).\r\n    let mut intent = AttachIntent::Control;\r\n    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:\r\n    // controller input that races ahead of the reply is BUFFERED, not dropped\r\n    // (the replayed-input-after-restart case) and not leaked to the PTY before\r\n    // we know we control the session (the busy-refuse case).\r\n    let mut role: Option<ServeRole> = None;\r\n    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();\r\n    // The attached session — captured from the Request so the terminal paths can\r\n    // explicitly release this worker's role (clearing driven_by / viewer_count\r\n    // promptly, not only on connection drop).\r\n    let mut attached_session: Option<u64> = None;\r\n    // This worker's attach GENERATION (ADR-0038 Amendment fix 6) — set when\r\n    // its Request lands; every terminal release carries it so a STALE worker\r\n    // (an older generation) can never clear a replacement controller.\r\n    let mut serve_gen: u64 = 0;\r\n    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the\r\n    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees\r\n    // intermittent live bursts, never a CPU spin.\r\n    let mut last_resubscribe: Option<Instant> = None;\r\n\r\n    // Durable-opener seed (ADR-0038 B): process the dispatcher-pinned Request\r\n    // BEFORE the event loop, through the exact wire-Request path — a rolled\r\n    // ring can no longer replay it. Non-Request seeds are ignored (the opener\r\n    // of an attach stream is always its Request).\r\n    if let Some(AttachRecord::Request {\r\n        session_id,\r\n        from_seq,\r\n        intent: req_intent,\r\n        endpoint_id: _,\r\n        gen: req_gen,\r\n    }) = seed_request\r\n    {\r\n        if let Some(out) = handle_attach_request(\r\n            brain,\r\n            &mut wire,\r\n            stream_id,\r\n            endpoint,\r\n            origin_node,\r\n            remote_session,\r\n            session_id,\r\n            from_seq,\r\n            req_intent,\r\n            req_gen,\r\n            &mut intent,\r\n            &mut attached_session,\r\n            &mut attached,\r\n            &mut serve_gen,\r\n        )? {\r\n            return Ok(out);\r\n        }\r\n    }\r\n\r\n    loop {\r\n        // The CONTROLLER resumes-from-floor on a b4 drop-don't-block output-gap\r\n        // (RE-FETCH from delivered_through, NOT snap — B2: a controller is\r\n        // authoritative). A VIEWER never reaches the reject-gap (its serve-brain is\r\n        // snap-above armed at attach, REQ-HAZARD-VIEWER-RING-ROLL-SNAP). Before a\r\n        // role resolves no Output flows, so a plain read is correct then.\r\n        // [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\r\n        let event = match (role, attached_session) {\r\n            (Some(ServeRole::Controller), Some(sid)) => {\r\n                match read_controller_event_resuming(brain, sid, serve_gen, 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                            gen: req_gen,\r\n                        } => {\r\n                            if let Some(out) = handle_attach_request(\r\n                                brain,\r\n                                &mut wire,\r\n                                stream_id,\r\n                                endpoint,\r\n                                origin_node,\r\n                                remote_session,\r\n                                session_id,\r\n                                from_seq,\r\n                                req_intent,\r\n                                req_gen,\r\n                                &mut intent,\r\n                                &mut attached_session,\r\n                                &mut attached,\r\n                                &mut serve_gen,\r\n                            )? {\r\n                                return Ok(out);\r\n                            }\r\n                        }\r\n                        AttachRecord::Input { data_b64, op_id }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            let bytes = decode_bytes(&data_b64)\r\n                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\r\n                            match role {\r\n                                // Confirmed controller — exactly-once at the PTY\r\n                                // (the operator-minted id rides the B5 journal). NO-ACK\r\n                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is\r\n                                // fire-and-forward (the ack is discarded here) and can\r\n                                // flood N input frames on this one conn; an acked write\r\n                                // back would fill the return direction and deadlock the\r\n                                // broker's per-conn handler. Dedup still holds at the\r\n                                // broker (op_id applied-set).\r\n                                // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]\r\n                                Some(ServeRole::Controller) => {\r\n                                    // The op_id is operator-minted (rc attach OpMinter),\r\n                                    // so it carries the `rc` journal tag (ADR-0034).\r\n                                    brain.send_effect_no_ack(\r\n                                        MintedOp::new(Minter::Rc, op_id),\r\n                                        &bytes,\r\n                                    )?\r\n                                }\r\n                                // Outcome not yet known — buffer; flushed on a\r\n                                // controller confirmation, discarded on busy/viewer.\r\n                                None => pending_input.push((op_id, bytes)),\r\n                                // A viewer never reaches here (intent gate).\r\n                                Some(ServeRole::Viewer) => {}\r\n                            }\r\n                        }\r\n                        AttachRecord::Resize { rows, cols }\r\n                            if attached && intent != AttachIntent::Viewer =>\r\n                        {\r\n                            // Controller → PTY resize (controller-EXCLUSIVE; the\r\n                            // broker rejects it on a viewer's stream). A viewer's\r\n                            // Resize is dropped here by the intent gate too.\r\n                            let _ = brain.resize(rows, cols);\r\n                        }\r\n                        // Input/Resize before Request, a viewer's input/resize,\r\n                        // or target-direction records echoed back: noise — ignore.\r\n                        _ => {}\r\n                    }\r\n                }\r\n            }\r\n\r\n            // ── Broker → loop: the resolved controller/viewer role. ─────────\r\n            BrokerEvent::Subscribed { outcome, .. } => {\r\n                use crate::msg::SubscribeOutcome as O;\r\n                match outcome {\r\n                    O::Controller | O::TookControl => {\r\n                        role = Some(ServeRole::Controller);\r\n                        // A driver attached — the wake edge (controller-only;\r\n                        // idempotent on an already-active seat).\r\n                        feed_rest(endpoint, crate::resting::RestEvent::Wake);\r\n                        // Flush any input that raced ahead of the reply (operator-\r\n                        // minted ops carry the `rc` tag).\r\n                        for (op, b) in pending_input.drain(..) {\r\n                            brain.send_effect(MintedOp::new(Minter::Rc, op), &b)?;\r\n                        }\r\n                    }\r\n                    O::Viewer => {\r\n                        role = Some(ServeRole::Viewer);\r\n                        // Wake-neutral: a viewer never wakes the endpoint, never\r\n                        // keeps it awake (Gap B). Discard any racing input.\r\n                        pending_input.clear();\r\n                    }\r\n                    O::BusyControlled { .. } => {\r\n                        // A different operator controls it — refuse (no PTY input\r\n                        // ever reached it; the buffer is discarded). The rc\r\n                        // renders the busy guidance client-side from driven_by.\r\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n            BrokerEvent::Size { rows, cols, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            // This controller was kicked by a different operator's `--take`.\r\n            BrokerEvent::Displaced { by, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                // No dormant edge: the endpoint stays driven by the taker (whose\r\n                // own serve loop already fired the wake).\r\n                return Ok(AttachServeOutcome::Displaced);\r\n            }\r\n\r\n            // This VIEWER overflowed its broker subscription queue and was evicted\r\n            // (it forwards slower than the drain fans out under flood). SKIP TO\r\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\r\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\r\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\r\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\r\n            // spin. The next live Output carries a forward seq jump; the operator's\r\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\r\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n            BrokerEvent::ViewerEvicted { session_id: sid }\r\n                if attached && Some(sid) == attached_session =>\r\n            {\r\n                if let Some(prev) = last_resubscribe {\r\n                    let since = prev.elapsed();\r\n                    if since < RESUBSCRIBE_INTERVAL {\r\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\r\n                    }\r\n                }\r\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\r\n                last_resubscribe = Some(Instant::now());\r\n            }\r\n\r\n            // The operator closed its send side: the viewport is over.\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\r\n                // info.json, carrying the current driven_by through), THEN release\r\n                // the role over IPC so the broker's driven_by=None is the LAST\r\n                // write — otherwise a concurrent resting RMW restores the stale\r\n                // marker (the clear-race the broker-single-writer model averts).\r\n                if role == Some(ServeRole::Controller) {\r\n                    // The driver detached — the endpoint rests warm (controller-\r\n                    // only; a viewer leaving is wake-neutral).\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                if let Some(sid) = attached_session {\r\n…\n                }\n…\n            }\n…\n        }\n    }\n}","startLine":277,"lineNumbers":[277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,null,595,null,597,null,626,627,628]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\attach.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":728,"totalBytes":17923,"outputLines":317,"outputBytes":17923,"shownRange":{"start":277,"end":593},"nextOffset":594}}},"isError":false,"timestamp":1784365494735}}
{"type":"message","id":"012ff88c","parentId":"6abfb760","timestamp":"2026-07-18T09:04:54.785Z","message":{"role":"toolResult","toolCallId":"call_33JIq9zLNt7gTyZKdehLGP3l|fc_0ce5441d29b9f05d016a5b41b69c5c819b9e0f2897344c032c","toolName":"read","content":[{"type":"text","text":"impl OutputLog {\n…\n    fn stamp_viewer_count(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);\n    }\n\n    /// Reap-time unlatch: a control/viewer stamp never outlives its session\n    /// (REQ-HAZARD-CONTROL-STAMP-LIFETIME). `/exit` kills the CHILD, not the\n    /// controller connection, so the OutputLog drops with the controller slot\n    /// still occupied — [`clear_controller`]'s detach path never runs, and\n    /// `controlled` / `driven_by` / `viewer_count` would otherwise latch in\n    /// info.json forever (and gossip a stale `controller_node` cross-node). The\n    /// exit-waiter calls this on reap so every teardown path clears what attach\n    /// stamped. Unconditional (idempotent when already clear) — the session is\n    /// dead, so there is no live controller to re-stamp concurrently (same\n    /// race-free argument as the W5 no-session self-heal). Best-effort, like the\n    /// sibling stampers; empty endpoint ⇒ nowhere to record.\n    // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n    // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\n    fn stamp_reaped(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        // ADR-0041 decision 4: a reap is a DEFINITIVE no-session truth — bump\n        // the endpoint's stamp generation UNDER the stamp-write serial, then\n        // clear. A KIND_SESSIONS poll that snapshotted this session PRE-reap now\n        // fails its generation check and can never relatch `controlled=true`\n        // onto the perch of a session that no longer exists (the emphasys C3\n        // poll-vs-reap window: no later poll would ever heal the latch).\n        let slot = stamp_slot(&self.endpoint);\n        let _write = slot.write.lock().unwrap_or_else(|p| p.into_inner());\n        slot.gen.fetch_add(1, Ordering::AcqRel);\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        let _ = spt_store::info::set_driven_by(&perch, None);\n        let _ = spt_store::info::set_controlled(&perch, false);\n        let _ = spt_store::info::set_viewer_count(&perch, 0);\n    }\n\n    /// Whether `sub` is the current controller (the resize-exclusivity check).\n    fn is_controller(&self, sub: &SharedSend) -> bool {\n        self.controller\n            .as_ref()\n            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))\n    }\n\n    /// The next seq a re-attaching controller still needs (the persisted resume\n    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`). W1: read from the\n    /// atomic the controller writer thread advances.\n    fn delivered_through(&self) -> u64 {\n        self.delivered_through.load(Ordering::Acquire)\n    }\n}\n\n/// A viewer's dedicated writer thread (REQ-HAZARD-VIEWER-ISOLATION): write the\n/// `initial` batch (size frame + ring replay) once, then drain live frames from\n/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the\n/// thread; the drain thread's next `try_send` then sees `Disconnected` and\n/// evicts the row. The drain thread itself never blocks on this socket.\nfn viewer_writer(\n    send: SharedSend,\n    session_id: u64,\n    initial: Vec<Envelope>,\n    rx: Receiver<Envelope>,\n    evicted: Arc<AtomicBool>,\n) {\n    // Bounded writer-exit record on every return path (F-039 leg d) — RAII so\n    // the reason is stamped exactly once however the thread ends.\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    struct ExitNote {\n        send: SharedSend,\n        session_id: u64,\n        reason: &'static str,\n    }\n    impl Drop for ExitNote {\n        fn drop(&mut self) {\n            self.send.lifecycle_event(\n                \"writer-exit\",\n                &format!(\"role=viewer session={} reason={}\", self.session_id, self.reason),\n            );\n        }\n    }\n    let mut note = ExitNote {\n        send: Arc::clone(&send),\n        session_id,\n        reason: \"write-failed\",\n    };\n    for frame in &initial {\n        // Bounded, cancelable conn write (round-4): a viewer that stops\n        // reading poisons + retires ITS conn at the deadline instead of\n        // parking this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        if !matches!(send.write(frame), Ok(ConnWrite::Done)) {\n            return;\n        }\n    }\n    while let Ok(frame) = rx.recv() {\n        if !matches!(send.write(&frame), Ok(ConnWrite::Done)) {\n            return;\n        }\n    }\n    note.reason = \"channel-closed\";\n    // `tx` dropped. If this was an EVICTION for overflow (append set the flag\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\n    // a session-exit EOF — so the serving brain skips to live (re-subscribes from\n    // the ring floor) instead of dying silently. A NORMAL teardown (session end /\n    // conn drop) leaves the flag false → no marker, exit quietly. This write is in\n    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4\n    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\n    if evicted.load(Ordering::Acquire) {\n        let marker = evicted_envelope(session_id);\n        let _ = send.write(&marker);\n    }\n}\n\n/// Round-4 observability bridge: a controller writer whose bounded conn write\n/// was DEADLINE-retired (`TimedOut` — the conn watchdog aborted a wedged\n/// write, or a gate-wait behind one exhausted the bound) IS the\n/// brain-subscriber stall-evict fact the W2 tally counts. Pre-round-4 that\n/// wedge aged until a logical sampler (`stall_evict_controller`) observed it;\n/// the physical bound now fires FIRST, so the tally is recorded here — same\n/// event class, same `daemon status` surface. An ORGANIC write failure (peer\n/// death, any non-TimedOut error) stays uncounted, exactly as before.\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\n    if e.kind() != io::ErrorKind::TimedOut {\n        return;\n    }\n    // conn id + stamps (F-039 leg b). [impl->REQ-CONN-POISON-ATTRIBUTION]\n    eprintln!(\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\n        crate::conn::log_stamp()\n    );\n    record_stall_evict();\n}\n\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\n/// chunk and advances the authoritative `delivered_through` on a successful\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\n/// does NOT move the resume cursor.\nenum CtrlMsg {\n    Output(u64, Envelope),\n    Control(Envelope),\n}\n\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\n/// overlap during a controller displace.\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\n    let mut cur = dt.load(Ordering::Acquire);\n    loop {\n        let next = advanced_cursor(cur, seq, true);\n        if next == cur {\n            return;\n        }\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\n            Ok(_) => return,\n            Err(actual) => cur = actual,\n        }\n    }\n}\n\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\n/// live frame is contiguous) this is identical to [`advance_delivered`].\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\n}\n\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\n/// write each — so a backed-up controller blocks ONLY this thread, never the\n/// session drain (which used to write the controller inline under the log lock).\n/// A failed write (dead operator) ends the thread; the drain's next bounded\n/// send then sees `Disconnected` and detaches the row. `Output` advances\n/// `delivered_through`; `Control` (Displaced/…) does not.\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\nfn controller_writer(\n    sid: u64,\n    send: SharedSend,\n    initial: Vec<(u64, Vec<u8>)>,\n    rx: Receiver<CtrlMsg>,\n    delivered_through: Arc<AtomicU64>,\n    epoch: u64,\n    controller_epoch: Arc<AtomicU64>,\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\n) {\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\n    // move the wedge onto the mutex).\n    let mark_blocked = |on: bool| {\n        if let Ok(mut g) = write_blocked_since.lock() {\n            *g = if on { Some(Instant::now()) } else { None };\n        }\n    };\n    for (seq, bytes) in &initial {\n        let frame = output_envelope(sid, *seq, bytes);\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n        // epoch past ours) must write NOTHING further — otherwise its owned\n        // initial batch races the new writer on the same socket and reorders the\n        // seq stream. The gate closure runs AFTER the conn's serialized write\n        // gate is acquired and BEFORE any byte is written — the same\n        // serialization point the old epoch-check-under-`send.lock()` had: both\n        // writers serialize on the one conn gate, and `become_controller` bumps\n        // the epoch BEFORE spawning the new writer, so whichever superseded\n        // writer next holds the gate observes the bump and bails before\n        // committing — no check-then-block-then-write window. (A frame the\n        // stale writer committed BEFORE the bump is irreducible at the broker;\n        // fix #1 removes the contradictory double-take that produced it.)\n        // Round-4: the write itself is bounded + cancelable — a non-draining\n        // consumer poisons + retires the physical conn at the deadline instead\n        // of wedging this thread in the OS write forever.\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n        mark_blocked(true);\n        let outcome =\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\n        mark_blocked(false);\n        match outcome {\n            Ok(ConnWrite::Done) => {}\n            Ok(ConnWrite::Superseded) => {\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=superseded\"),\n                );\n                return;\n            }\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                send.lifecycle_event(\n                    \"writer-exit\",\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\n                );\n                return;\n            }\n        }\n        advance_delivered(&delivered_through, *seq);\n    }\n    while let Ok(msg) = rx.recv() {\n        let (frame, seq) = match &msg {\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\n            CtrlMsg::Control(frame) => (frame, None),\n        };\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\n        // a superseded writer's channel holds only its pre-supersede output\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\n        // control frames — notably the `Displaced` kick the displaced controller\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\n        mark_blocked(true);\n        let res = send.write(frame);\n        mark_blocked(false);\n        match res {\n            Ok(_) => {}\n            Err(e) => {\n                note_controller_write_retired(sid, send.id(), &e);\n                // Bounded writer-exit record (F-039 leg d).\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\n                send.lifecycle_event(\n                    \"writer-exit\",\n…\n                );\n…\n            }\n        }\n…\n    }\n…\n}\n\n[Showing lines 1479-1758 of 6771. Use :1759 to continue]"}],"details":{"truncation":{"content":"        if self.endpoint.is_empty() {\r\n            return;\r\n        }\r\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\r\n        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);\r\n    }\r\n\r\n    /// Reap-time unlatch: a control/viewer stamp never outlives its session\r\n    /// (REQ-HAZARD-CONTROL-STAMP-LIFETIME). `/exit` kills the CHILD, not the\r\n    /// controller connection, so the OutputLog drops with the controller slot\r\n    /// still occupied — [`clear_controller`]'s detach path never runs, and\r\n    /// `controlled` / `driven_by` / `viewer_count` would otherwise latch in\r\n    /// info.json forever (and gossip a stale `controller_node` cross-node). The\r\n    /// exit-waiter calls this on reap so every teardown path clears what attach\r\n    /// stamped. Unconditional (idempotent when already clear) — the session is\r\n    /// dead, so there is no live controller to re-stamp concurrently (same\r\n    /// race-free argument as the W5 no-session self-heal). Best-effort, like the\r\n    /// sibling stampers; empty endpoint ⇒ nowhere to record.\r\n    // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n    // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\n    fn stamp_reaped(&self) {\r\n        if self.endpoint.is_empty() {\r\n            return;\r\n        }\r\n        // ADR-0041 decision 4: a reap is a DEFINITIVE no-session truth — bump\r\n        // the endpoint's stamp generation UNDER the stamp-write serial, then\r\n        // clear. A KIND_SESSIONS poll that snapshotted this session PRE-reap now\r\n        // fails its generation check and can never relatch `controlled=true`\r\n        // onto the perch of a session that no longer exists (the emphasys C3\r\n        // poll-vs-reap window: no later poll would ever heal the latch).\r\n        let slot = stamp_slot(&self.endpoint);\r\n        let _write = slot.write.lock().unwrap_or_else(|p| p.into_inner());\r\n        slot.gen.fetch_add(1, Ordering::AcqRel);\r\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\r\n        let _ = spt_store::info::set_driven_by(&perch, None);\r\n        let _ = spt_store::info::set_controlled(&perch, false);\r\n        let _ = spt_store::info::set_viewer_count(&perch, 0);\r\n    }\r\n\r\n    /// Whether `sub` is the current controller (the resize-exclusivity check).\r\n    fn is_controller(&self, sub: &SharedSend) -> bool {\r\n        self.controller\r\n            .as_ref()\r\n            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))\r\n    }\r\n\r\n    /// The next seq a re-attaching controller still needs (the persisted resume\r\n    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`). W1: read from the\r\n    /// atomic the controller writer thread advances.\r\n    fn delivered_through(&self) -> u64 {\r\n        self.delivered_through.load(Ordering::Acquire)\r\n    }\r\n}\r\n\r\n/// A viewer's dedicated writer thread (REQ-HAZARD-VIEWER-ISOLATION): write the\r\n/// `initial` batch (size frame + ring replay) once, then drain live frames from\r\n/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the\r\n/// thread; the drain thread's next `try_send` then sees `Disconnected` and\r\n/// evicts the row. The drain thread itself never blocks on this socket.\r\nfn viewer_writer(\r\n    send: SharedSend,\r\n    session_id: u64,\r\n    initial: Vec<Envelope>,\r\n    rx: Receiver<Envelope>,\r\n    evicted: Arc<AtomicBool>,\r\n) {\r\n    // Bounded writer-exit record on every return path (F-039 leg d) — RAII so\r\n    // the reason is stamped exactly once however the thread ends.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    struct ExitNote {\r\n        send: SharedSend,\r\n        session_id: u64,\r\n        reason: &'static str,\r\n    }\r\n    impl Drop for ExitNote {\r\n        fn drop(&mut self) {\r\n            self.send.lifecycle_event(\r\n                \"writer-exit\",\r\n                &format!(\"role=viewer session={} reason={}\", self.session_id, self.reason),\r\n            );\r\n        }\r\n    }\r\n    let mut note = ExitNote {\r\n        send: Arc::clone(&send),\r\n        session_id,\r\n        reason: \"write-failed\",\r\n    };\r\n    for frame in &initial {\r\n        // Bounded, cancelable conn write (round-4): a viewer that stops\r\n        // reading poisons + retires ITS conn at the deadline instead of\r\n        // parking this thread in the OS write forever.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        if !matches!(send.write(frame), Ok(ConnWrite::Done)) {\r\n            return;\r\n        }\r\n    }\r\n    while let Ok(frame) = rx.recv() {\r\n        if !matches!(send.write(&frame), Ok(ConnWrite::Done)) {\r\n            return;\r\n        }\r\n    }\r\n    note.reason = \"channel-closed\";\r\n    // `tx` dropped. If this was an EVICTION for overflow (append set the flag\r\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\r\n    // a session-exit EOF — so the serving brain skips to live (re-subscribes from\r\n    // the ring floor) instead of dying silently. A NORMAL teardown (session end /\r\n    // conn drop) leaves the flag false → no marker, exit quietly. This write is in\r\n    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4\r\n    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.\r\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n    if evicted.load(Ordering::Acquire) {\r\n        let marker = evicted_envelope(session_id);\r\n        let _ = send.write(&marker);\r\n    }\r\n}\r\n\r\n/// Round-4 observability bridge: a controller writer whose bounded conn write\r\n/// was DEADLINE-retired (`TimedOut` — the conn watchdog aborted a wedged\r\n/// write, or a gate-wait behind one exhausted the bound) IS the\r\n/// brain-subscriber stall-evict fact the W2 tally counts. Pre-round-4 that\r\n/// wedge aged until a logical sampler (`stall_evict_controller`) observed it;\r\n/// the physical bound now fires FIRST, so the tally is recorded here — same\r\n/// event class, same `daemon status` surface. An ORGANIC write failure (peer\r\n/// death, any non-TimedOut error) stays uncounted, exactly as before.\r\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\r\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\r\n    if e.kind() != io::ErrorKind::TimedOut {\r\n        return;\r\n    }\r\n    // conn id + stamps (F-039 leg b). [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    eprintln!(\r\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\r\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\r\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\r\n        crate::conn::log_stamp()\r\n    );\r\n    record_stall_evict();\r\n}\r\n\r\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\r\n/// chunk and advances the authoritative `delivered_through` on a successful\r\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\r\n/// does NOT move the resume cursor.\r\nenum CtrlMsg {\r\n    Output(u64, Envelope),\r\n    Control(Envelope),\r\n}\r\n\r\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\r\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\r\n/// overlap during a controller displace.\r\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\r\n    let mut cur = dt.load(Ordering::Acquire);\r\n    loop {\r\n        let next = advanced_cursor(cur, seq, true);\r\n        if next == cur {\r\n            return;\r\n        }\r\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\r\n            Ok(_) => return,\r\n            Err(actual) => cur = actual,\r\n        }\r\n    }\r\n}\r\n\r\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\r\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\r\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\r\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\r\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\r\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\r\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\r\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\r\n/// live frame is contiguous) this is identical to [`advance_delivered`].\r\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\r\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\r\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\r\n}\r\n\r\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\r\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\r\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\r\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\r\n/// write each — so a backed-up controller blocks ONLY this thread, never the\r\n/// session drain (which used to write the controller inline under the log lock).\r\n/// A failed write (dead operator) ends the thread; the drain's next bounded\r\n/// send then sees `Disconnected` and detaches the row. `Output` advances\r\n/// `delivered_through`; `Control` (Displaced/…) does not.\r\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\r\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\r\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\r\nfn controller_writer(\r\n    sid: u64,\r\n    send: SharedSend,\r\n    initial: Vec<(u64, Vec<u8>)>,\r\n    rx: Receiver<CtrlMsg>,\r\n    delivered_through: Arc<AtomicU64>,\r\n    epoch: u64,\r\n    controller_epoch: Arc<AtomicU64>,\r\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\r\n) {\r\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\r\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\r\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\r\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\r\n    // move the wedge onto the mutex).\r\n    let mark_blocked = |on: bool| {\r\n        if let Ok(mut g) = write_blocked_since.lock() {\r\n            *g = if on { Some(Instant::now()) } else { None };\r\n        }\r\n    };\r\n    for (seq, bytes) in &initial {\r\n        let frame = output_envelope(sid, *seq, bytes);\r\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\r\n        // epoch past ours) must write NOTHING further — otherwise its owned\r\n        // initial batch races the new writer on the same socket and reorders the\r\n        // seq stream. The gate closure runs AFTER the conn's serialized write\r\n        // gate is acquired and BEFORE any byte is written — the same\r\n        // serialization point the old epoch-check-under-`send.lock()` had: both\r\n        // writers serialize on the one conn gate, and `become_controller` bumps\r\n        // the epoch BEFORE spawning the new writer, so whichever superseded\r\n        // writer next holds the gate observes the bump and bails before\r\n        // committing — no check-then-block-then-write window. (A frame the\r\n        // stale writer committed BEFORE the bump is irreducible at the broker;\r\n        // fix #1 removes the contradictory double-take that produced it.)\r\n        // Round-4: the write itself is bounded + cancelable — a non-draining\r\n        // consumer poisons + retires the physical conn at the deadline instead\r\n        // of wedging this thread in the OS write forever.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        mark_blocked(true);\r\n        let outcome =\r\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\r\n        mark_blocked(false);\r\n        match outcome {\r\n            Ok(ConnWrite::Done) => {}\r\n            Ok(ConnWrite::Superseded) => {\r\n                // Bounded writer-exit record (F-039 leg d).\r\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=superseded\"),\r\n                );\r\n                return;\r\n            }\r\n            Err(e) => {\r\n                note_controller_write_retired(sid, send.id(), &e);\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\r\n                );\r\n                return;\r\n            }\r\n        }\r\n        advance_delivered(&delivered_through, *seq);\r\n    }\r\n    while let Ok(msg) = rx.recv() {\r\n        let (frame, seq) = match &msg {\r\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\r\n            CtrlMsg::Control(frame) => (frame, None),\r\n        };\r\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\r\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\r\n        // a superseded writer's channel holds only its pre-supersede output\r\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\r\n        // control frames — notably the `Displaced` kick the displaced controller\r\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\r\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\r\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\r\n        mark_blocked(true);\r\n        let res = send.write(frame);\r\n        mark_blocked(false);\r\n        match res {\r\n            Ok(_) => {}\r\n            Err(e) => {\r\n                note_controller_write_retired(sid, send.id(), &e);\r\n                // Bounded writer-exit record (F-039 leg d).\r\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r","truncated":true,"truncatedBy":"lines","totalLines":5293,"totalBytes":14044,"outputLines":280,"outputBytes":14044,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"impl OutputLog {\n…\n    fn stamp_viewer_count(&self) {\n        if self.endpoint.is_empty() {\r\n            return;\r\n        }\r\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\r\n        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);\r\n    }\r\n\r\n    /// Reap-time unlatch: a control/viewer stamp never outlives its session\r\n    /// (REQ-HAZARD-CONTROL-STAMP-LIFETIME). `/exit` kills the CHILD, not the\r\n    /// controller connection, so the OutputLog drops with the controller slot\r\n    /// still occupied — [`clear_controller`]'s detach path never runs, and\r\n    /// `controlled` / `driven_by` / `viewer_count` would otherwise latch in\r\n    /// info.json forever (and gossip a stale `controller_node` cross-node). The\r\n    /// exit-waiter calls this on reap so every teardown path clears what attach\r\n    /// stamped. Unconditional (idempotent when already clear) — the session is\r\n    /// dead, so there is no live controller to re-stamp concurrently (same\r\n    /// race-free argument as the W5 no-session self-heal). Best-effort, like the\r\n    /// sibling stampers; empty endpoint ⇒ nowhere to record.\r\n    // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n    // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\n    fn stamp_reaped(&self) {\r\n        if self.endpoint.is_empty() {\r\n            return;\r\n        }\r\n        // ADR-0041 decision 4: a reap is a DEFINITIVE no-session truth — bump\r\n        // the endpoint's stamp generation UNDER the stamp-write serial, then\r\n        // clear. A KIND_SESSIONS poll that snapshotted this session PRE-reap now\r\n        // fails its generation check and can never relatch `controlled=true`\r\n        // onto the perch of a session that no longer exists (the emphasys C3\r\n        // poll-vs-reap window: no later poll would ever heal the latch).\r\n        let slot = stamp_slot(&self.endpoint);\r\n        let _write = slot.write.lock().unwrap_or_else(|p| p.into_inner());\r\n        slot.gen.fetch_add(1, Ordering::AcqRel);\r\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\r\n        let _ = spt_store::info::set_driven_by(&perch, None);\r\n        let _ = spt_store::info::set_controlled(&perch, false);\r\n        let _ = spt_store::info::set_viewer_count(&perch, 0);\r\n    }\r\n\r\n    /// Whether `sub` is the current controller (the resize-exclusivity check).\r\n    fn is_controller(&self, sub: &SharedSend) -> bool {\r\n        self.controller\r\n            .as_ref()\r\n            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))\r\n    }\r\n\r\n    /// The next seq a re-attaching controller still needs (the persisted resume\r\n    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`). W1: read from the\r\n    /// atomic the controller writer thread advances.\r\n    fn delivered_through(&self) -> u64 {\r\n        self.delivered_through.load(Ordering::Acquire)\r\n    }\r\n}\r\n\r\n/// A viewer's dedicated writer thread (REQ-HAZARD-VIEWER-ISOLATION): write the\r\n/// `initial` batch (size frame + ring replay) once, then drain live frames from\r\n/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the\r\n/// thread; the drain thread's next `try_send` then sees `Disconnected` and\r\n/// evicts the row. The drain thread itself never blocks on this socket.\r\nfn viewer_writer(\r\n    send: SharedSend,\r\n    session_id: u64,\r\n    initial: Vec<Envelope>,\r\n    rx: Receiver<Envelope>,\r\n    evicted: Arc<AtomicBool>,\r\n) {\r\n    // Bounded writer-exit record on every return path (F-039 leg d) — RAII so\r\n    // the reason is stamped exactly once however the thread ends.\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    struct ExitNote {\r\n        send: SharedSend,\r\n        session_id: u64,\r\n        reason: &'static str,\r\n    }\r\n    impl Drop for ExitNote {\r\n        fn drop(&mut self) {\r\n            self.send.lifecycle_event(\r\n                \"writer-exit\",\r\n                &format!(\"role=viewer session={} reason={}\", self.session_id, self.reason),\r\n            );\r\n        }\r\n    }\r\n    let mut note = ExitNote {\r\n        send: Arc::clone(&send),\r\n        session_id,\r\n        reason: \"write-failed\",\r\n    };\r\n    for frame in &initial {\r\n        // Bounded, cancelable conn write (round-4): a viewer that stops\r\n        // reading poisons + retires ITS conn at the deadline instead of\r\n        // parking this thread in the OS write forever.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        if !matches!(send.write(frame), Ok(ConnWrite::Done)) {\r\n            return;\r\n        }\r\n    }\r\n    while let Ok(frame) = rx.recv() {\r\n        if !matches!(send.write(&frame), Ok(ConnWrite::Done)) {\r\n            return;\r\n        }\r\n    }\r\n    note.reason = \"channel-closed\";\r\n    // `tx` dropped. If this was an EVICTION for overflow (append set the flag\r\n    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from\r\n    // a session-exit EOF — so the serving brain skips to live (re-subscribes from\r\n    // the ring floor) instead of dying silently. A NORMAL teardown (session end /\r\n    // conn drop) leaves the flag false → no marker, exit quietly. This write is in\r\n    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4\r\n    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.\r\n    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n    if evicted.load(Ordering::Acquire) {\r\n        let marker = evicted_envelope(session_id);\r\n        let _ = send.write(&marker);\r\n    }\r\n}\r\n\r\n/// Round-4 observability bridge: a controller writer whose bounded conn write\r\n/// was DEADLINE-retired (`TimedOut` — the conn watchdog aborted a wedged\r\n/// write, or a gate-wait behind one exhausted the bound) IS the\r\n/// brain-subscriber stall-evict fact the W2 tally counts. Pre-round-4 that\r\n/// wedge aged until a logical sampler (`stall_evict_controller`) observed it;\r\n/// the physical bound now fires FIRST, so the tally is recorded here — same\r\n/// event class, same `daemon status` surface. An ORGANIC write failure (peer\r\n/// death, any non-TimedOut error) stays uncounted, exactly as before.\r\n// [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\r\nfn note_controller_write_retired(sid: u64, conn: u64, e: &io::Error) {\r\n    if e.kind() != io::ErrorKind::TimedOut {\r\n        return;\r\n    }\r\n    // conn id + stamps (F-039 leg b). [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    eprintln!(\r\n        \"BRAIN_SUBSCRIBER_STALL_EVICT:{sid} conn={conn} {} — controller conn write \\\r\n         exceeded its bound; physical conn poisoned + retired (writer released, EOF \\\r\n         cleanup follows) [REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\",\r\n        crate::conn::log_stamp()\r\n    );\r\n    record_stall_evict();\r\n}\r\n\r\n/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced\r\n/// chunk and advances the authoritative `delivered_through` on a successful\r\n/// write; `Control` is an out-of-band notice (Displaced, …) that is written but\r\n/// does NOT move the resume cursor.\r\nenum CtrlMsg {\r\n    Output(u64, Envelope),\r\n    Control(Envelope),\r\n}\r\n\r\n/// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically\r\n/// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer\r\n/// overlap during a controller displace.\r\nfn advance_delivered(dt: &AtomicU64, seq: u64) {\r\n    let mut cur = dt.load(Ordering::Acquire);\r\n    loop {\r\n        let next = advanced_cursor(cur, seq, true);\r\n        if next == cur {\r\n            return;\r\n        }\r\n        match dt.compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Acquire) {\r\n            Ok(_) => return,\r\n            Err(actual) => cur = actual,\r\n        }\r\n    }\r\n}\r\n\r\n/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to\r\n/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap\r\n/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,\r\n/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching\r\n/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the\r\n/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An\r\n/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,\r\n/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every\r\n/// live frame is contiguous) this is identical to [`advance_delivered`].\r\n// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]\r\nfn contiguous_advance(dt: &AtomicU64, seq: u64) {\r\n    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);\r\n}\r\n\r\n/// The interactive CONTROLLER's dedicated writer thread (W1 /\r\n/// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of\r\n/// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume\r\n/// cursor on each success), then drain live [`CtrlMsg`]s with a BLOCKING socket\r\n/// write each — so a backed-up controller blocks ONLY this thread, never the\r\n/// session drain (which used to write the controller inline under the log lock).\r\n/// A failed write (dead operator) ends the thread; the drain's next bounded\r\n/// send then sees `Disconnected` and detaches the row. `Output` advances\r\n/// `delivered_through`; `Control` (Displaced/…) does not.\r\n// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]\r\n// [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]\r\n#[allow(clippy::too_many_arguments)] // per-writer thread-handoff channels (W2 added the blocked-since signal); a struct would obscure the call\r\nfn controller_writer(\r\n    sid: u64,\r\n    send: SharedSend,\r\n    initial: Vec<(u64, Vec<u8>)>,\r\n    rx: Receiver<CtrlMsg>,\r\n    delivered_through: Arc<AtomicU64>,\r\n    epoch: u64,\r\n    controller_epoch: Arc<AtomicU64>,\r\n    write_blocked_since: Arc<Mutex<Option<Instant>>>,\r\n) {\r\n    // W2 decouple (REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE): mark the exact window a\r\n    // BLOCKING socket write is in flight, so a reader can tell a WEDGED writer (brain\r\n    // conn suspended) from a merely idle one and stall-evict it. Set BEFORE the write,\r\n    // cleared AFTER — the mutex is never held across `write_frame` (that would just\r\n    // move the wedge onto the mutex).\r\n    let mark_blocked = |on: bool| {\r\n        if let Ok(mut g) = write_blocked_since.lock() {\r\n            *g = if on { Some(Instant::now()) } else { None };\r\n        }\r\n    };\r\n    for (seq, bytes) in &initial {\r\n        let frame = output_envelope(sid, *seq, bytes);\r\n        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\r\n        // epoch past ours) must write NOTHING further — otherwise its owned\r\n        // initial batch races the new writer on the same socket and reorders the\r\n        // seq stream. The gate closure runs AFTER the conn's serialized write\r\n        // gate is acquired and BEFORE any byte is written — the same\r\n        // serialization point the old epoch-check-under-`send.lock()` had: both\r\n        // writers serialize on the one conn gate, and `become_controller` bumps\r\n        // the epoch BEFORE spawning the new writer, so whichever superseded\r\n        // writer next holds the gate observes the bump and bails before\r\n        // committing — no check-then-block-then-write window. (A frame the\r\n        // stale writer committed BEFORE the bump is irreducible at the broker;\r\n        // fix #1 removes the contradictory double-take that produced it.)\r\n        // Round-4: the write itself is bounded + cancelable — a non-draining\r\n        // consumer poisons + retires the physical conn at the deadline instead\r\n        // of wedging this thread in the OS write forever.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        mark_blocked(true);\r\n        let outcome =\r\n            send.write_gated(&frame, || controller_epoch.load(Ordering::Acquire) == epoch);\r\n        mark_blocked(false);\r\n        match outcome {\r\n            Ok(ConnWrite::Done) => {}\r\n            Ok(ConnWrite::Superseded) => {\r\n                // Bounded writer-exit record (F-039 leg d).\r\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=superseded\"),\r\n                );\r\n                return;\r\n            }\r\n            Err(e) => {\r\n                note_controller_write_retired(sid, send.id(), &e);\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n                    &format!(\"role=controller session={sid} reason=write-failed kind={:?}\", e.kind()),\r\n                );\r\n                return;\r\n            }\r\n        }\r\n        advance_delivered(&delivered_through, *seq);\r\n    }\r\n    while let Ok(msg) = rx.recv() {\r\n        let (frame, seq) = match &msg {\r\n            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),\r\n            CtrlMsg::Control(frame) => (frame, None),\r\n        };\r\n        // NO epoch gate on the live path (P1c): new output only ever flows to the\r\n        // CURRENT controller's channel (the drain clones `self.controller.tx`), so\r\n        // a superseded writer's channel holds only its pre-supersede output\r\n        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL\r\n        // control frames — notably the `Displaced` kick the displaced controller\r\n        // MUST still receive. The loop ends naturally when `tx` drops. Gating\r\n        // output here would suppress that `Displaced` (the cv-matrix loud-take\r\n        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.\r\n        mark_blocked(true);\r\n        let res = send.write(frame);\r\n        mark_blocked(false);\r\n        match res {\r\n            Ok(_) => {}\r\n            Err(e) => {\r\n                note_controller_write_retired(sid, send.id(), &e);\r\n                // Bounded writer-exit record (F-039 leg d).\r\n                // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n                send.lifecycle_event(\r\n                    \"writer-exit\",\r\n…\n                );\n…\n            }\n        }\n…\n    }\n…\n}","startLine":792,"lineNumbers":[792,null,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,null,1760,null,1762,1763,null,1772,null,1779]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6771,"totalBytes":14044,"outputLines":280,"outputBytes":14044,"shownRange":{"start":1479,"end":1758},"nextOffset":1759}}},"isError":false,"timestamp":1784365494785}}
{"type":"message","id":"28615d62","parentId":"012ff88c","timestamp":"2026-07-18T09:04:54.833Z","message":{"role":"toolResult","toolCallId":"call_B5EA5I37N3xrJ3OuwA80hP4q|fc_0ce5441d29b9f05d016a5b41b69c60819b87bcfab8335c60c6","toolName":"read","content":[{"type":"text","text":"    pub fn net(&self) -> Option<&NetHost> {\n        self.net.get()\n    }\n\n    /// Accept connections forever, handling each on its own thread. The broker\n    /// is the long-lived process; this is its main loop. Per-connection\n    /// threading is half the KH 7.4 isolation stance: one brain's blocking\n    /// dispatch can never stall another's (the per-session half is\n    /// [`Broker::dispatch_spawn`]'s drain/waiter threads).\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n    pub fn serve(self: &Arc<Self>) -> io::Result<()> {\n        loop {\n            let conn = self.listener.accept()?;\n            // C1 graceful stop: [`Broker::stop`] raises the latch then dials the\n            // throwaway connection that woke this accept — observe it and return so the\n            // serve thread (and, with it, the listener) tears down instead of leaking.\n            if self.stop.load(Ordering::Acquire) {\n                return Ok(());\n            }\n            let me = Arc::clone(self);\n            // NAMED (ADR-0040 decision 7): conn handlers were the second\n            // unnamed population in the hertz thread census.\n            // [impl->REQ-SEAT-LIFETIME-BOUNDED]\n            let _ = thread::Builder::new()\n                .name(\"broker-conn\".to_string())\n                .spawn(move || {\n                    let _ = me.handle_conn(conn);\n                });\n        }\n    }\n\n    /// Gracefully stop the [`serve`](Self::serve) accept loop: raise the latch, then\n    /// dial one throwaway connection so the blocked `accept()` wakes, observes the\n    /// latch, and returns (dropping the listener). Best-effort + idempotent — a second\n    /// call, or a call after the loop already exited, just fails the dial harmlessly.\n    /// Fills the honest gap that `serve` is otherwise unstoppable (the leaked-serve\n    /// load that starved the seed stop barrier — REQ-HAZARD-DAEMON-STOP-BARRIER).\n    // [impl->REQ-HAZARD-DAEMON-STOP-BARRIER]\n    pub fn stop(&self) {\n        self.stop.store(true, Ordering::Release);\n        let _ = LocalSocketTransport::connect(&self.name);\n    }\n\n    /// Accept exactly one connection and handle it on a spawned thread, returning\n    /// that thread's handle. The single-brain entry point the tests exercise.\n    pub fn serve_once(self: &Arc<Self>) -> io::Result<JoinHandle<()>> {\n        let conn = self.listener.accept()?;\n        let me = Arc::clone(self);\n        Ok(thread::spawn(move || {\n            let _ = me.handle_conn(conn);\n        }))\n    }\n\n    /// Number of currently hosted sessions (for tests / introspection).\n    pub fn session_count(&self) -> usize {\n        recover(&self.sessions).len()\n    }\n\n    /// TEST-SUPPORT seam (round-4 false_promote rig, doyle Option C ruling\n    /// `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`): install a STATICALLY-\n    /// wedged LOCAL controller (`by: None`) on hosted session `id` — a sink\n    /// whose `write_blocked_since` is seeded `blocked_ms_ago` in the past and\n    /// whose writer thread only drains its channel (it NEVER calls\n    /// [`BrokerConn::write`], so no watchdog is armed and the round-4 physical\n    /// bound cannot clear the injected wedge). Lets an integration rig\n    /// exercise the REQ-UPDATE-PROMOTE-DRAINED gate DECISION in isolation —\n    /// the physical bound itself is `brain_decouple.rs`'s concern. The wedge\n    /// drains through the REAL logical paths (a take's `stall_evict_controller`\n    /// or a `KIND_SESSIONS` reap), exactly like a production stall-evict.\n    /// Returns false if the session doesn't exist or the seed can't be built.\n    /// `#[doc(hidden)]` — test support, not a product surface.\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\n    #[doc(hidden)]\n    pub fn test_seed_wedged_local_controller(&self, id: u64, blocked_ms_ago: u64) -> bool {\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&id) {\n                Some(h) => Arc::clone(&h.log),\n                None => return false,\n            }\n        };\n        let Some(blocked) = Instant::now().checked_sub(Duration::from_millis(blocked_ms_ago))\n        else {\n            return false;\n        };\n        // A real (immediately-abandoned) socket pair for the sink's SharedSend\n        // identity — nothing ever writes to it (the fixture invariant), so the\n        // dropped peer is irrelevant; the stall signal is the injected\n        // `write_blocked_since`, not a real socket block.\n        static SEED_SEQ: AtomicU32 = AtomicU32::new(0);\n        let name = format!(\n            \"spt-daemon-wedgeseed-{}-{}.sock\",\n            std::process::id(),\n            SEED_SEQ.fetch_add(1, Ordering::Relaxed)\n        );\n        let Ok(listener) = LocalSocketTransport::bind(&name) else {\n            return false;\n        };\n        let Ok(_client) = LocalSocketTransport::connect(&name) else {\n            return false;\n        };\n        let Ok(server) = listener.accept() else {\n            return false;\n        };\n        let (_recv, send_half) = server.split();\n        let send: SharedSend = Arc::new(BrokerConn::new(send_half, brain_write_deadline()));\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n        // Drain-forever (never a socket write, never finishes while tx lives):\n        // stays un-reapable as a live-but-wedged writer even under child output.\n        let writer = thread::spawn(move || for _ in rx.iter() {});\n        recover_log(&log).controller = Some(ControllerSink {\n            tx,\n            send,\n            attach_gen: 0,\n            by: None,\n            last_ok: Instant::now(),\n            write_blocked_since: Arc::new(Mutex::new(Some(blocked))),\n            _writer: writer,\n        });\n        true\n    }\n\n    /// TEST-SUPPORT seam for the round-3 structural resume guard (stale-guard\n    /// swap, doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md`): the\n    /// count of read-only viewers currently attached to hosted session `id`,\n    /// or `None` if no session with that id is hosted. Lets the deterministic\n    /// zero-subscriber resume guard assert — with no timing window — that the\n    /// daemon brain's cursor-only resume attaches NO subscriber to any session,\n    /// while the paired hazard arm (a `resume_sessions` subscribe) is observed\n    /// to raise the same count to >= 1. Follows the exact locking idiom of\n    /// [`Self::test_seed_wedged_local_controller`] (`recover(&self.sessions)` →\n    /// `get(&id)` → `Arc::clone(&h.log)` → `recover_log`).\n    /// `#[doc(hidden)]` — test support, not a product surface.\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\n    #[doc(hidden)]\n    pub fn test_session_viewer_count(&self, id: u64) -> Option<usize> {\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&id) {\n                Some(h) => Arc::clone(&h.log),\n                None => return None,\n            }\n        };\n        let count = recover_log(&log).viewers.len();\n        Some(count)\n    }\n\n    /// Live per-connection handler threads (for tests / introspection). The\n    /// UPDATE-WEDGE round-4 gate watches this fall when a poisoned physical conn\n    /// drives its handler to EOF cleanup within the write bound — without the\n    /// black-holed client dropping its end.\n    // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\n    pub fn conn_handler_count(&self) -> usize {\n        self.conn_handlers.load(Ordering::Acquire)\n    }\n\n    /// The live **resident adapter children** (translation binaries) for\n    /// `endpoint` — the processes a live adapter update (W3d) stops and restarts\n    /// so the install dir's locked binaries free up. The broker `sessions` map IS\n    /// the per-endpoint resident registry (W3a, ADR-0025): each `HostedSession`\n    /// already carries its `endpoint` + lifecycle-managed `translation` child.\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\n    #[allow(dead_code)] // consumed by W3d `dispatch_adapter_apply` (next sub-commit)\n    fn resident_translations_for_endpoint(&self, endpoint: &str) -> Vec<Arc<Translation>> {\n        let map = recover(&self.sessions);\n        map.values()\n            .filter(|s| s.endpoint == endpoint)\n            .filter_map(|s| s.translation.clone())\n            .collect()\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        // Streams THIS conn opened as ConnectionBound (ADR-0040 decision 6):\n        // they die with the conn — the exit sweep FINs each toward its target\n        // and terminal-retires the local row. Durable rows never enter it.\n        // [impl->REQ-STREAM-LEASE-CLASSES]\n        let mut my_cb_streams: 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                // Fresh/RejectExisting spawn (ADR-0041 decision 5): a typed\n                // conflict reply carries no session — only a real Created\n                // auto-subscribes. TEST-RIG-ONLY valve (the N-1 loud-fail leg):\n                // with SPT_TEST_SPAWN_FRESH_UNSUPPORTED set this broker answers\n                // EXACTLY like an N-1 broker that predates the verb — the\n                // forward-compat unknown-kind reply, byte-identical. Never set\n                // in production.\n                // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n                KIND_SPAWN_FRESH => {\n                    if std::env::var(\"SPT_TEST_SPAWN_FRESH_UNSUPPORTED\").is_ok() {\n                        send_error(&send, &format!(\"unknown command kind {:?}\", env.kind));\n                    } else {\n                        match self.dispatch_spawn_fresh(env, &send) {\n                            Ok(Some(id)) => my_subs.push(id),\n                            Ok(None) => {} // typed conflict already sent\n                            Err(msg) => send_error(&send, &msg),\n                        }\n                    }\n                }\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\n                    Ok(id) => my_subs.push(id),\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_UNSUBSCRIBE => {\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_INPUT => {\n                    if let Err(msg) = self.dispatch_input(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_RESIZE => {\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_KILL => {\n                    if let Err(msg) = self.dispatch_kill(env) {\n                        send_error(&send, &msg);\n                    }\n                }\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\n                KIND_ADAPTER_APPLY => {\n…\n                }\n…\n            }\n        }\n…\n    }\n\n[Showing lines 2899-3243 of 6771. Use :3244 to continue]"}],"details":{"truncation":{"content":"        self.net.get()\r\n    }\r\n\r\n    /// Accept connections forever, handling each on its own thread. The broker\r\n    /// is the long-lived process; this is its main loop. Per-connection\r\n    /// threading is half the KH 7.4 isolation stance: one brain's blocking\r\n    /// dispatch can never stall another's (the per-session half is\r\n    /// [`Broker::dispatch_spawn`]'s drain/waiter threads).\r\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\r\n    pub fn serve(self: &Arc<Self>) -> io::Result<()> {\r\n        loop {\r\n            let conn = self.listener.accept()?;\r\n            // C1 graceful stop: [`Broker::stop`] raises the latch then dials the\r\n            // throwaway connection that woke this accept — observe it and return so the\r\n            // serve thread (and, with it, the listener) tears down instead of leaking.\r\n            if self.stop.load(Ordering::Acquire) {\r\n                return Ok(());\r\n            }\r\n            let me = Arc::clone(self);\r\n            // NAMED (ADR-0040 decision 7): conn handlers were the second\r\n            // unnamed population in the hertz thread census.\r\n            // [impl->REQ-SEAT-LIFETIME-BOUNDED]\r\n            let _ = thread::Builder::new()\r\n                .name(\"broker-conn\".to_string())\r\n                .spawn(move || {\r\n                    let _ = me.handle_conn(conn);\r\n                });\r\n        }\r\n    }\r\n\r\n    /// Gracefully stop the [`serve`](Self::serve) accept loop: raise the latch, then\r\n    /// dial one throwaway connection so the blocked `accept()` wakes, observes the\r\n    /// latch, and returns (dropping the listener). Best-effort + idempotent — a second\r\n    /// call, or a call after the loop already exited, just fails the dial harmlessly.\r\n    /// Fills the honest gap that `serve` is otherwise unstoppable (the leaked-serve\r\n    /// load that starved the seed stop barrier — REQ-HAZARD-DAEMON-STOP-BARRIER).\r\n    // [impl->REQ-HAZARD-DAEMON-STOP-BARRIER]\r\n    pub fn stop(&self) {\r\n        self.stop.store(true, Ordering::Release);\r\n        let _ = LocalSocketTransport::connect(&self.name);\r\n    }\r\n\r\n    /// Accept exactly one connection and handle it on a spawned thread, returning\r\n    /// that thread's handle. The single-brain entry point the tests exercise.\r\n    pub fn serve_once(self: &Arc<Self>) -> io::Result<JoinHandle<()>> {\r\n        let conn = self.listener.accept()?;\r\n        let me = Arc::clone(self);\r\n        Ok(thread::spawn(move || {\r\n            let _ = me.handle_conn(conn);\r\n        }))\r\n    }\r\n\r\n    /// Number of currently hosted sessions (for tests / introspection).\r\n    pub fn session_count(&self) -> usize {\r\n        recover(&self.sessions).len()\r\n    }\r\n\r\n    /// TEST-SUPPORT seam (round-4 false_promote rig, doyle Option C ruling\r\n    /// `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`): install a STATICALLY-\r\n    /// wedged LOCAL controller (`by: None`) on hosted session `id` — a sink\r\n    /// whose `write_blocked_since` is seeded `blocked_ms_ago` in the past and\r\n    /// whose writer thread only drains its channel (it NEVER calls\r\n    /// [`BrokerConn::write`], so no watchdog is armed and the round-4 physical\r\n    /// bound cannot clear the injected wedge). Lets an integration rig\r\n    /// exercise the REQ-UPDATE-PROMOTE-DRAINED gate DECISION in isolation —\r\n    /// the physical bound itself is `brain_decouple.rs`'s concern. The wedge\r\n    /// drains through the REAL logical paths (a take's `stall_evict_controller`\r\n    /// or a `KIND_SESSIONS` reap), exactly like a production stall-evict.\r\n    /// Returns false if the session doesn't exist or the seed can't be built.\r\n    /// `#[doc(hidden)]` — test support, not a product surface.\r\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\r\n    #[doc(hidden)]\r\n    pub fn test_seed_wedged_local_controller(&self, id: u64, blocked_ms_ago: u64) -> bool {\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => return false,\r\n            }\r\n        };\r\n        let Some(blocked) = Instant::now().checked_sub(Duration::from_millis(blocked_ms_ago))\r\n        else {\r\n            return false;\r\n        };\r\n        // A real (immediately-abandoned) socket pair for the sink's SharedSend\r\n        // identity — nothing ever writes to it (the fixture invariant), so the\r\n        // dropped peer is irrelevant; the stall signal is the injected\r\n        // `write_blocked_since`, not a real socket block.\r\n        static SEED_SEQ: AtomicU32 = AtomicU32::new(0);\r\n        let name = format!(\r\n            \"spt-daemon-wedgeseed-{}-{}.sock\",\r\n            std::process::id(),\r\n            SEED_SEQ.fetch_add(1, Ordering::Relaxed)\r\n        );\r\n        let Ok(listener) = LocalSocketTransport::bind(&name) else {\r\n            return false;\r\n        };\r\n        let Ok(_client) = LocalSocketTransport::connect(&name) else {\r\n            return false;\r\n        };\r\n        let Ok(server) = listener.accept() else {\r\n            return false;\r\n        };\r\n        let (_recv, send_half) = server.split();\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send_half, brain_write_deadline()));\r\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\r\n        // Drain-forever (never a socket write, never finishes while tx lives):\r\n        // stays un-reapable as a live-but-wedged writer even under child output.\r\n        let writer = thread::spawn(move || for _ in rx.iter() {});\r\n        recover_log(&log).controller = Some(ControllerSink {\r\n            tx,\r\n            send,\r\n            attach_gen: 0,\r\n            by: None,\r\n            last_ok: Instant::now(),\r\n            write_blocked_since: Arc::new(Mutex::new(Some(blocked))),\r\n            _writer: writer,\r\n        });\r\n        true\r\n    }\r\n\r\n    /// TEST-SUPPORT seam for the round-3 structural resume guard (stale-guard\r\n    /// swap, doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md`): the\r\n    /// count of read-only viewers currently attached to hosted session `id`,\r\n    /// or `None` if no session with that id is hosted. Lets the deterministic\r\n    /// zero-subscriber resume guard assert — with no timing window — that the\r\n    /// daemon brain's cursor-only resume attaches NO subscriber to any session,\r\n    /// while the paired hazard arm (a `resume_sessions` subscribe) is observed\r\n    /// to raise the same count to >= 1. Follows the exact locking idiom of\r\n    /// [`Self::test_seed_wedged_local_controller`] (`recover(&self.sessions)` →\r\n    /// `get(&id)` → `Arc::clone(&h.log)` → `recover_log`).\r\n    /// `#[doc(hidden)]` — test support, not a product surface.\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    #[doc(hidden)]\r\n    pub fn test_session_viewer_count(&self, id: u64) -> Option<usize> {\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => return None,\r\n            }\r\n        };\r\n        let count = recover_log(&log).viewers.len();\r\n        Some(count)\r\n    }\r\n\r\n    /// Live per-connection handler threads (for tests / introspection). The\r\n    /// UPDATE-WEDGE round-4 gate watches this fall when a poisoned physical conn\r\n    /// drives its handler to EOF cleanup within the write bound — without the\r\n    /// black-holed client dropping its end.\r\n    // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n    pub fn conn_handler_count(&self) -> usize {\r\n        self.conn_handlers.load(Ordering::Acquire)\r\n    }\r\n\r\n    /// The live **resident adapter children** (translation binaries) for\r\n    /// `endpoint` — the processes a live adapter update (W3d) stops and restarts\r\n    /// so the install dir's locked binaries free up. The broker `sessions` map IS\r\n    /// the per-endpoint resident registry (W3a, ADR-0025): each `HostedSession`\r\n    /// already carries its `endpoint` + lifecycle-managed `translation` child.\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    #[allow(dead_code)] // consumed by W3d `dispatch_adapter_apply` (next sub-commit)\r\n    fn resident_translations_for_endpoint(&self, endpoint: &str) -> Vec<Arc<Translation>> {\r\n        let map = recover(&self.sessions);\r\n        map.values()\r\n            .filter(|s| s.endpoint == endpoint)\r\n            .filter_map(|s| s.translation.clone())\r\n            .collect()\r\n    }\r\n\r\n    /// The live endpoints running `adapter`, each with its install dir — the apply\r\n    /// targets for a live adapter update (W3d decides daemon-apply vs CLI-direct\r\n    /// from whether this is empty). Distinct, sorted. (W3a, ADR-0025)\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    #[allow(dead_code)] // consumed by W3d `dispatch_adapter_apply` (next sub-commit)\r\n    fn endpoints_running_adapter(&self, adapter: &str) -> Vec<(String, std::path::PathBuf)> {\r\n        let rows: Vec<_> = {\r\n            let map = recover(&self.sessions);\r\n            map.values()\r\n                .map(|s| (s.endpoint.clone(), s.adapter.clone(), s.install_dir.clone()))\r\n                .collect()\r\n        };\r\n        select_endpoints_running_adapter(rows, adapter)\r\n    }\r\n\r\n    /// The child pid of a hosted session, if present (tests assert it is stable\r\n    /// across brain restarts).\r\n    pub fn session_pid(&self, id: u64) -> Option<u32> {\r\n        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .and_then(|h| h.session.process_id())\r\n    }\r\n\r\n    /// Broker-truth read for the brain promotion DRAINED gate\r\n    /// (REQ-UPDATE-PROMOTE-DRAINED): is ANY hosted session's LOCAL controller (the\r\n    /// brain's own `by: None` conn) still WEDGED — blocked in a socket write past the\r\n    /// brain-write deadline? The brain supervisor (in THIS broker process — no round-\r\n    /// trip) promotes a ready update candidate only once this reads `false`: an OLD\r\n    /// generation whose local controller conn is black-holed reads wedged until the W2\r\n    /// stall-evict releases it, so `brain.ready` inside the 15 s window can no longer\r\n    /// promote onto a still-frozen control plane (the 22:47 false-promote). A cleanly-\r\n    /// closed old conn is simply absent → `false` at once (the normal fast promote).\r\n    /// Follows the established sessions→log lock order (KIND_SESSIONS reap); reads\r\n    /// only `write_blocked_since` under each log lock, never any I/O.\r\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\r\n    pub fn any_local_controller_wedged(&self) -> bool {\r\n        let now = Instant::now();\r\n        let deadline = brain_write_deadline();\r\n        let sessions = match self.sessions.lock() {\r\n            Ok(g) => g,\r\n            Err(p) => p.into_inner(),\r\n        };\r\n        sessions.values().any(|h| {\r\n            match h.log.lock() {\r\n                Ok(log) => log.local_controller_wedged(now, deadline),\r\n                // A poisoned log lock cannot be honestly read — treat as NOT wedged\r\n                // (fail toward promotion, never a permanent gate that strands apply).\r\n                Err(_) => false,\r\n            }\r\n        })\r\n    }\r\n\r\n    /// The count of live QUIC connections the broker-owned net endpoint holds\r\n    /// (D4a, ADR-0004 §B), or 0 when net-less. A direct read of the broker's own\r\n    /// state — the survival-assertion seam for the D7-1 process-level E2E: a QUIC\r\n    /// connection the broker holds must outlive a brain-PROCESS swap (restoration\r\n    /// D7-1), the net sibling of [`session_count`](Self::session_count).\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    pub fn net_conn_count(&self) -> usize {\r\n        self.net.get().map(|h| h.conn_count() as usize).unwrap_or(0)\r\n    }\r\n\r\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\r\n    /// absent. It advances every time the child emits output — the\r\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\r\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\r\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\r\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\r\n    /// peek, like [`session_pid`](Self::session_pid).\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\r\n        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .map(|h| recover_log(&h.log).next_seq)\r\n    }\r\n\r\n    /// Drive one brain connection: handshake, then dispatch its command frames\r\n    /// until it disconnects. On disconnect, detach this connection from any\r\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\r\n    /// restart must not disturb the hosted children).\r\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\r\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\r\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\r\n        struct HandlerTally<'a>(&'a AtomicUsize);\r\n        impl Drop for HandlerTally<'_> {\r\n            fn drop(&mut self) {\r\n                self.0.fetch_sub(1, Ordering::AcqRel);\r\n            }\r\n        }\r\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\r\n        let _tally = HandlerTally(&self.conn_handlers);\r\n\r\n        let (mut recv, send) = conn.split();\r\n        // Round-4: the send half is owned by the bounded/cancelable conn\r\n        // primitive from birth — every write on this physical connection is\r\n        // deadline-bounded, and a wedge poisons + retires the conn.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\r\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\r\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\r\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        send.describe(\"role=brain\");\r\n\r\n        // Version handshake: this connection must be a brain (2.3).\r\n        recv_hello(&mut recv, Role::Brain)?;\r\n\r\n        let mut my_subs: Vec<u64> = Vec::new();\r\n        let mut my_stream_subs: Vec<u64> = Vec::new();\r\n        // Streams THIS conn opened as ConnectionBound (ADR-0040 decision 6):\r\n        // they die with the conn — the exit sweep FINs each toward its target\r\n        // and terminal-retires the local row. Durable rows never enter it.\r\n        // [impl->REQ-STREAM-LEASE-CLASSES]\r\n        let mut my_cb_streams: Vec<u64> = Vec::new();\r\n        let mut my_presence_sub = false;\r\n\r\n        loop {\r\n            let env = match read_frame(&mut recv) {\r\n                Ok(e) => e,\r\n                // Any read failure (EOF included) ends the connection.\r\n                Err(_) => break,\r\n            };\r\n            match env.kind.as_str() {\r\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                // Fresh/RejectExisting spawn (ADR-0041 decision 5): a typed\r\n                // conflict reply carries no session — only a real Created\r\n                // auto-subscribes. TEST-RIG-ONLY valve (the N-1 loud-fail leg):\r\n                // with SPT_TEST_SPAWN_FRESH_UNSUPPORTED set this broker answers\r\n                // EXACTLY like an N-1 broker that predates the verb — the\r\n                // forward-compat unknown-kind reply, byte-identical. Never set\r\n                // in production.\r\n                // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\r\n                KIND_SPAWN_FRESH => {\r\n                    if std::env::var(\"SPT_TEST_SPAWN_FRESH_UNSUPPORTED\").is_ok() {\r\n                        send_error(&send, &format!(\"unknown command kind {:?}\", env.kind));\r\n                    } else {\r\n                        match self.dispatch_spawn_fresh(env, &send) {\r\n                            Ok(Some(id)) => my_subs.push(id),\r\n                            Ok(None) => {} // typed conflict already sent\r\n                            Err(msg) => send_error(&send, &msg),\r\n                        }\r\n                    }\r\n                }\r\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_UNSUBSCRIBE => {\r\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_INPUT => {\r\n                    if let Err(msg) = self.dispatch_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_RESIZE => {\r\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r","truncated":true,"truncatedBy":"lines","totalLines":3873,"totalBytes":17420,"outputLines":345,"outputBytes":17420,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    pub fn net(&self) -> Option<&NetHost> {\n        self.net.get()\r\n    }\r\n\r\n    /// Accept connections forever, handling each on its own thread. The broker\r\n    /// is the long-lived process; this is its main loop. Per-connection\r\n    /// threading is half the KH 7.4 isolation stance: one brain's blocking\r\n    /// dispatch can never stall another's (the per-session half is\r\n    /// [`Broker::dispatch_spawn`]'s drain/waiter threads).\r\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\r\n    pub fn serve(self: &Arc<Self>) -> io::Result<()> {\r\n        loop {\r\n            let conn = self.listener.accept()?;\r\n            // C1 graceful stop: [`Broker::stop`] raises the latch then dials the\r\n            // throwaway connection that woke this accept — observe it and return so the\r\n            // serve thread (and, with it, the listener) tears down instead of leaking.\r\n            if self.stop.load(Ordering::Acquire) {\r\n                return Ok(());\r\n            }\r\n            let me = Arc::clone(self);\r\n            // NAMED (ADR-0040 decision 7): conn handlers were the second\r\n            // unnamed population in the hertz thread census.\r\n            // [impl->REQ-SEAT-LIFETIME-BOUNDED]\r\n            let _ = thread::Builder::new()\r\n                .name(\"broker-conn\".to_string())\r\n                .spawn(move || {\r\n                    let _ = me.handle_conn(conn);\r\n                });\r\n        }\r\n    }\r\n\r\n    /// Gracefully stop the [`serve`](Self::serve) accept loop: raise the latch, then\r\n    /// dial one throwaway connection so the blocked `accept()` wakes, observes the\r\n    /// latch, and returns (dropping the listener). Best-effort + idempotent — a second\r\n    /// call, or a call after the loop already exited, just fails the dial harmlessly.\r\n    /// Fills the honest gap that `serve` is otherwise unstoppable (the leaked-serve\r\n    /// load that starved the seed stop barrier — REQ-HAZARD-DAEMON-STOP-BARRIER).\r\n    // [impl->REQ-HAZARD-DAEMON-STOP-BARRIER]\r\n    pub fn stop(&self) {\r\n        self.stop.store(true, Ordering::Release);\r\n        let _ = LocalSocketTransport::connect(&self.name);\r\n    }\r\n\r\n    /// Accept exactly one connection and handle it on a spawned thread, returning\r\n    /// that thread's handle. The single-brain entry point the tests exercise.\r\n    pub fn serve_once(self: &Arc<Self>) -> io::Result<JoinHandle<()>> {\r\n        let conn = self.listener.accept()?;\r\n        let me = Arc::clone(self);\r\n        Ok(thread::spawn(move || {\r\n            let _ = me.handle_conn(conn);\r\n        }))\r\n    }\r\n\r\n    /// Number of currently hosted sessions (for tests / introspection).\r\n    pub fn session_count(&self) -> usize {\r\n        recover(&self.sessions).len()\r\n    }\r\n\r\n    /// TEST-SUPPORT seam (round-4 false_promote rig, doyle Option C ruling\r\n    /// `docs/UPDATE-WEDGE-R4-FALSE-PROMOTE-RULING.md`): install a STATICALLY-\r\n    /// wedged LOCAL controller (`by: None`) on hosted session `id` — a sink\r\n    /// whose `write_blocked_since` is seeded `blocked_ms_ago` in the past and\r\n    /// whose writer thread only drains its channel (it NEVER calls\r\n    /// [`BrokerConn::write`], so no watchdog is armed and the round-4 physical\r\n    /// bound cannot clear the injected wedge). Lets an integration rig\r\n    /// exercise the REQ-UPDATE-PROMOTE-DRAINED gate DECISION in isolation —\r\n    /// the physical bound itself is `brain_decouple.rs`'s concern. The wedge\r\n    /// drains through the REAL logical paths (a take's `stall_evict_controller`\r\n    /// or a `KIND_SESSIONS` reap), exactly like a production stall-evict.\r\n    /// Returns false if the session doesn't exist or the seed can't be built.\r\n    /// `#[doc(hidden)]` — test support, not a product surface.\r\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\r\n    #[doc(hidden)]\r\n    pub fn test_seed_wedged_local_controller(&self, id: u64, blocked_ms_ago: u64) -> bool {\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => return false,\r\n            }\r\n        };\r\n        let Some(blocked) = Instant::now().checked_sub(Duration::from_millis(blocked_ms_ago))\r\n        else {\r\n            return false;\r\n        };\r\n        // A real (immediately-abandoned) socket pair for the sink's SharedSend\r\n        // identity — nothing ever writes to it (the fixture invariant), so the\r\n        // dropped peer is irrelevant; the stall signal is the injected\r\n        // `write_blocked_since`, not a real socket block.\r\n        static SEED_SEQ: AtomicU32 = AtomicU32::new(0);\r\n        let name = format!(\r\n            \"spt-daemon-wedgeseed-{}-{}.sock\",\r\n            std::process::id(),\r\n            SEED_SEQ.fetch_add(1, Ordering::Relaxed)\r\n        );\r\n        let Ok(listener) = LocalSocketTransport::bind(&name) else {\r\n            return false;\r\n        };\r\n        let Ok(_client) = LocalSocketTransport::connect(&name) else {\r\n            return false;\r\n        };\r\n        let Ok(server) = listener.accept() else {\r\n            return false;\r\n        };\r\n        let (_recv, send_half) = server.split();\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send_half, brain_write_deadline()));\r\n        let (tx, rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\r\n        // Drain-forever (never a socket write, never finishes while tx lives):\r\n        // stays un-reapable as a live-but-wedged writer even under child output.\r\n        let writer = thread::spawn(move || for _ in rx.iter() {});\r\n        recover_log(&log).controller = Some(ControllerSink {\r\n            tx,\r\n            send,\r\n            attach_gen: 0,\r\n            by: None,\r\n            last_ok: Instant::now(),\r\n            write_blocked_since: Arc::new(Mutex::new(Some(blocked))),\r\n            _writer: writer,\r\n        });\r\n        true\r\n    }\r\n\r\n    /// TEST-SUPPORT seam for the round-3 structural resume guard (stale-guard\r\n    /// swap, doyle ruling `docs/UPDATE-WEDGE-R4-STALE-GUARD-RULING.md`): the\r\n    /// count of read-only viewers currently attached to hosted session `id`,\r\n    /// or `None` if no session with that id is hosted. Lets the deterministic\r\n    /// zero-subscriber resume guard assert — with no timing window — that the\r\n    /// daemon brain's cursor-only resume attaches NO subscriber to any session,\r\n    /// while the paired hazard arm (a `resume_sessions` subscribe) is observed\r\n    /// to raise the same count to >= 1. Follows the exact locking idiom of\r\n    /// [`Self::test_seed_wedged_local_controller`] (`recover(&self.sessions)` →\r\n    /// `get(&id)` → `Arc::clone(&h.log)` → `recover_log`).\r\n    /// `#[doc(hidden)]` — test support, not a product surface.\r\n    // [impl->REQ-BRAIN-RESUME-NO-CONN-DEADLOCK]\r\n    #[doc(hidden)]\r\n    pub fn test_session_viewer_count(&self, id: u64) -> Option<usize> {\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => return None,\r\n            }\r\n        };\r\n        let count = recover_log(&log).viewers.len();\r\n        Some(count)\r\n    }\r\n\r\n    /// Live per-connection handler threads (for tests / introspection). The\r\n    /// UPDATE-WEDGE round-4 gate watches this fall when a poisoned physical conn\r\n    /// drives its handler to EOF cleanup within the write bound — without the\r\n    /// black-holed client dropping its end.\r\n    // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n    pub fn conn_handler_count(&self) -> usize {\r\n        self.conn_handlers.load(Ordering::Acquire)\r\n    }\r\n\r\n    /// The live **resident adapter children** (translation binaries) for\r\n    /// `endpoint` — the processes a live adapter update (W3d) stops and restarts\r\n    /// so the install dir's locked binaries free up. The broker `sessions` map IS\r\n    /// the per-endpoint resident registry (W3a, ADR-0025): each `HostedSession`\r\n    /// already carries its `endpoint` + lifecycle-managed `translation` child.\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    #[allow(dead_code)] // consumed by W3d `dispatch_adapter_apply` (next sub-commit)\r\n    fn resident_translations_for_endpoint(&self, endpoint: &str) -> Vec<Arc<Translation>> {\r\n        let map = recover(&self.sessions);\r\n        map.values()\r\n            .filter(|s| s.endpoint == endpoint)\r\n            .filter_map(|s| s.translation.clone())\r\n            .collect()\r\n    }\r\n\r\n    /// The live endpoints running `adapter`, each with its install dir — the apply\r\n    /// targets for a live adapter update (W3d decides daemon-apply vs CLI-direct\r\n    /// from whether this is empty). Distinct, sorted. (W3a, ADR-0025)\r\n    // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n    #[allow(dead_code)] // consumed by W3d `dispatch_adapter_apply` (next sub-commit)\r\n    fn endpoints_running_adapter(&self, adapter: &str) -> Vec<(String, std::path::PathBuf)> {\r\n        let rows: Vec<_> = {\r\n            let map = recover(&self.sessions);\r\n            map.values()\r\n                .map(|s| (s.endpoint.clone(), s.adapter.clone(), s.install_dir.clone()))\r\n                .collect()\r\n        };\r\n        select_endpoints_running_adapter(rows, adapter)\r\n    }\r\n\r\n    /// The child pid of a hosted session, if present (tests assert it is stable\r\n    /// across brain restarts).\r\n    pub fn session_pid(&self, id: u64) -> Option<u32> {\r\n        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .and_then(|h| h.session.process_id())\r\n    }\r\n\r\n    /// Broker-truth read for the brain promotion DRAINED gate\r\n    /// (REQ-UPDATE-PROMOTE-DRAINED): is ANY hosted session's LOCAL controller (the\r\n    /// brain's own `by: None` conn) still WEDGED — blocked in a socket write past the\r\n    /// brain-write deadline? The brain supervisor (in THIS broker process — no round-\r\n    /// trip) promotes a ready update candidate only once this reads `false`: an OLD\r\n    /// generation whose local controller conn is black-holed reads wedged until the W2\r\n    /// stall-evict releases it, so `brain.ready` inside the 15 s window can no longer\r\n    /// promote onto a still-frozen control plane (the 22:47 false-promote). A cleanly-\r\n    /// closed old conn is simply absent → `false` at once (the normal fast promote).\r\n    /// Follows the established sessions→log lock order (KIND_SESSIONS reap); reads\r\n    /// only `write_blocked_since` under each log lock, never any I/O.\r\n    // [impl->REQ-UPDATE-PROMOTE-DRAINED]\r\n    pub fn any_local_controller_wedged(&self) -> bool {\r\n        let now = Instant::now();\r\n        let deadline = brain_write_deadline();\r\n        let sessions = match self.sessions.lock() {\r\n            Ok(g) => g,\r\n            Err(p) => p.into_inner(),\r\n        };\r\n        sessions.values().any(|h| {\r\n            match h.log.lock() {\r\n                Ok(log) => log.local_controller_wedged(now, deadline),\r\n                // A poisoned log lock cannot be honestly read — treat as NOT wedged\r\n                // (fail toward promotion, never a permanent gate that strands apply).\r\n                Err(_) => false,\r\n            }\r\n        })\r\n    }\r\n\r\n    /// The count of live QUIC connections the broker-owned net endpoint holds\r\n    /// (D4a, ADR-0004 §B), or 0 when net-less. A direct read of the broker's own\r\n    /// state — the survival-assertion seam for the D7-1 process-level E2E: a QUIC\r\n    /// connection the broker holds must outlive a brain-PROCESS swap (restoration\r\n    /// D7-1), the net sibling of [`session_count`](Self::session_count).\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    pub fn net_conn_count(&self) -> usize {\r\n        self.net.get().map(|h| h.conn_count() as usize).unwrap_or(0)\r\n    }\r\n\r\n    /// The output-log high-water (`next_seq`) of a hosted session, or `None` if\r\n    /// absent. It advances every time the child emits output — the\r\n    /// **functional**-survival seam for the D7-1 process-level E2E (doyle F1):\r\n    /// after a brain-PROCESS swap, writing input to the broker-held PTY child and\r\n    /// watching THIS advance proves the path is alive end-to-end (input → child →\r\n    /// echo → broker ring), not merely that the handle is held. A read-only direct\r\n    /// peek, like [`session_pid`](Self::session_pid).\r\n    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\r\n    pub fn session_output_seq(&self, id: u64) -> Option<u64> {\r\n        self.sessions\r\n            .lock()\r\n            .unwrap()\r\n            .get(&id)\r\n            .map(|h| recover_log(&h.log).next_seq)\r\n    }\r\n\r\n    /// Drive one brain connection: handshake, then dispatch its command frames\r\n    /// until it disconnects. On disconnect, detach this connection from any\r\n    /// sessions it subscribed to — the sessions themselves persist (B2: a brain\r\n    /// restart must not disturb the hosted children).\r\n    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\r\n        // RAII handler tally (panic-safe): the round-4 gate's seam for \"the\r\n        // poisoned conn's handler reached EOF cleanup\" (see conn_handler_count).\r\n        struct HandlerTally<'a>(&'a AtomicUsize);\r\n        impl Drop for HandlerTally<'_> {\r\n            fn drop(&mut self) {\r\n                self.0.fetch_sub(1, Ordering::AcqRel);\r\n            }\r\n        }\r\n        self.conn_handlers.fetch_add(1, Ordering::AcqRel);\r\n        let _tally = HandlerTally(&self.conn_handlers);\r\n\r\n        let (mut recv, send) = conn.split();\r\n        // Round-4: the send half is owned by the bounded/cancelable conn\r\n        // primitive from birth — every write on this physical connection is\r\n        // deadline-bounded, and a wedge poisons + retires the conn.\r\n        // [impl->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK]\r\n        let send: SharedSend = Arc::new(BrokerConn::new(send, brain_write_deadline()));\r\n        // Base attribution fact (F-039 leg b): broker clients are brains; role\r\n        // facts (controller/viewer/presence/stream subscriber) accrete at the\r\n        // attach sites. [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n        send.describe(\"role=brain\");\r\n\r\n        // Version handshake: this connection must be a brain (2.3).\r\n        recv_hello(&mut recv, Role::Brain)?;\r\n\r\n        let mut my_subs: Vec<u64> = Vec::new();\r\n        let mut my_stream_subs: Vec<u64> = Vec::new();\r\n        // Streams THIS conn opened as ConnectionBound (ADR-0040 decision 6):\r\n        // they die with the conn — the exit sweep FINs each toward its target\r\n        // and terminal-retires the local row. Durable rows never enter it.\r\n        // [impl->REQ-STREAM-LEASE-CLASSES]\r\n        let mut my_cb_streams: Vec<u64> = Vec::new();\r\n        let mut my_presence_sub = false;\r\n\r\n        loop {\r\n            let env = match read_frame(&mut recv) {\r\n                Ok(e) => e,\r\n                // Any read failure (EOF included) ends the connection.\r\n                Err(_) => break,\r\n            };\r\n            match env.kind.as_str() {\r\n                KIND_SPAWN => match self.dispatch_spawn(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                // Fresh/RejectExisting spawn (ADR-0041 decision 5): a typed\r\n                // conflict reply carries no session — only a real Created\r\n                // auto-subscribes. TEST-RIG-ONLY valve (the N-1 loud-fail leg):\r\n                // with SPT_TEST_SPAWN_FRESH_UNSUPPORTED set this broker answers\r\n                // EXACTLY like an N-1 broker that predates the verb — the\r\n                // forward-compat unknown-kind reply, byte-identical. Never set\r\n                // in production.\r\n                // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\r\n                KIND_SPAWN_FRESH => {\r\n                    if std::env::var(\"SPT_TEST_SPAWN_FRESH_UNSUPPORTED\").is_ok() {\r\n                        send_error(&send, &format!(\"unknown command kind {:?}\", env.kind));\r\n                    } else {\r\n                        match self.dispatch_spawn_fresh(env, &send) {\r\n                            Ok(Some(id)) => my_subs.push(id),\r\n                            Ok(None) => {} // typed conflict already sent\r\n                            Err(msg) => send_error(&send, &msg),\r\n                        }\r\n                    }\r\n                }\r\n                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {\r\n                    Ok(id) => my_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_UNSUBSCRIBE => {\r\n                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_INPUT => {\r\n                    if let Err(msg) = self.dispatch_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_RESIZE => {\r\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r\n…\n                }\n…\n            }\n        }\n…\n    }","startLine":2898,"lineNumbers":[2898,2899,2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,null,3247,null,3460,3461,null,3509]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6771,"totalBytes":17420,"outputLines":345,"outputBytes":17420,"shownRange":{"start":2899,"end":3243},"nextOffset":3244}}},"isError":false,"timestamp":1784365494833}}
{"type":"message","id":"5dffd1e8","parentId":"28615d62","timestamp":"2026-07-18T09:05:05.113Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning extraction of grid operations 260-500**"},{"type":"toolCall","id":"call_siFsUWmxBfFXHp4lNkV0i0l8|fc_0ce5441d29b9f05d016a5b41c0fbf4819b81b818855d5ca6eb","name":"read","arguments":{"i":"Reading cell mutation semantics","path":"crates/spt-term/src/screen.rs","selector":"240-500"}},{"type":"toolCall","id":"call_xidwSyfc3e0Uu3g6mAsTOjSG|fc_0ce5441d29b9f05d016a5b41c0fc00819b8044e1a4e41fc257","name":"read","arguments":{"i":"Reading RC setup and cleanup","path":"crates/spt/src/rc.rs","selector":"750-870,1100-1370"}},{"type":"toolCall","id":"call_BCpoPBMW3NJUmtBkBBtzVogr|fc_0ce5441d29b9f05d016a5b41c0fc08819bb43dd75069d9244e","name":"read","arguments":{"i":"Reading RC reconnect and pump","path":"crates/spt/src/rc.rs","selector":"1390-1735,1740-2030"}},{"type":"toolCall","id":"call_f5orIrgxrs7dXTDNuYBfyhd4|fc_0ce5441d29b9f05d016a5b41c0fc10819b865cb3bc7c0f6b59","name":"read","arguments":{"i":"Reading serve terminal exits","path":"crates/spt-daemon/src/attach.rs","selector":"540-728"}},{"type":"toolCall","id":"call_k3MhmjWJ5bToC5p8L2PRjEos|fc_0ce5441d29b9f05d016a5b41c0fc14819bb2da97fe43b4d805","name":"read","arguments":{"i":"Reading broker disconnect sweep","path":"crates/spt-daemon/src/broker.rs","selector":"3235-3510"}},{"type":"toolCall","id":"call_R1ySxe7weSPvSMdp0XpAWauA|fc_0ce5441d29b9f05d016a5b41c0fc1c819bac7478c6d236de7c","name":"grep","arguments":{"i":"Finding output transformations","pattern":"decode_bytes|encode_bytes|data_b64|AttachRecord::Output|OutputEvent|output_envelope","path":"crates/spt/src/rc.rs; crates/spt-daemon/src/attach.rs; crates/spt-daemon/src/brain.rs; crates/spt-daemon/src/msg.rs; crates/spt-daemon/src/broker.rs; crates/spt-net/src/net/attach.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_cozkvEQP2IDSg4zz1tIfEAw8|fc_0ce5441d29b9f05d016a5b41c0fc20819b90c510cea2778c32","name":"grep","arguments":{"i":"Finding rc timeout constants","pattern":"const .*TIMEOUT|FIRST_EVENT|RECONNECT|POLL_SLICE|STALL|grace|deadline","path":"crates/spt/src/rc.rs; crates/spt-daemon/src/attach.rs; crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/conn.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":27775,"output":442,"cacheRead":90624,"cacheWrite":0,"totalTokens":118841,"reasoningTokens":15,"cost":{"input":0.138875,"output":0.013260000000000001,"cacheRead":0.045312,"cacheWrite":0,"total":0.19744699999999998}},"stopReason":"toolUse","timestamp":1784365494888,"responseId":"resp_0ce5441d29b9f05d016a5b41b73868819baf70b946771764b7","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b41b8f21c819ba966cd54b15f634b","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0HAZYsR61cuoi4_mJfduDXplZBO9eAYh7oM7lWStkFQsjGZGNEdESV6v6XN8-7XDcPFjI2FWuGMIM1DmkBFU3XvYiBZ7mLAieOfrfJa8vzELc8VP_PTHjmtC3aEYS3VUff9Z_NrQjbJU3mRRw4KPM2J081Ijh4HXKdyt1UZ72QEXXHEx1jMLKtc3irnASEYBh47JCPIxMl9W0X7LGUOF84i7mHjXKkDEOpY0yq64MhOeKZw8U4rmA4aNqcRpVwxZZKrSDyyfmRGcRlJiqh1FRwRvQEia9EacgidNVKKLBLMmMnOwaIFOrgeQsvZX4TL-G7Bb-vJe3ZUyBj6auCG6uMyOZQiKKa7yJdXJ0fZyDjHQLwHdfEXDVVee9fJxYZqtEJq0FzGa1B_BDZbEAxlMYhQGwtAkcUWj9Ys-DHIaoOEmwWGpaFkQY7dE9zHttvpC77JZzzTLNSeN59nJg6vVmlWSMxVbemkZHG_qTHn-PqtwDiz-wMRhkVnZdfb4IROcJ1Rx5SQkAEct2emjwggu5GO75Xu62MhJCFpkc405XE25m8EUY4VGwqAubdyTB3DYF9n2LHU2d1qUhS09kY3w0CE5jrR0T1v0IHWKWWvo3N5vtoSETKybdqwDBxmLHqMQEjN666AOI4zfCGgWv9sBR4g-AjKVd_UFkhi6GUYnW-1XOBGRq0NZH6F0VCN91zMYN7P_kClJJZMkPPPZhBxBTBZyBEqXwIR5wXxTTWEkpnjKvcnZkdPROkUaX0PTxU-YArxJSzsSFfu3j55Jv6WQyu_I7vjV72fqeRKfmHlz1Qaj8olQePP0LsxurPzVZX1wh6pQtY251IkDVGccPrLSD0BZI9vRWjqZB17a_38C_YNr-m1GS2bvwt9kmrUDxF0xMLmsfrIlO26Z_GPXJ49Dy7grTt8VYfK-BJa-TTcAmUdLPH_yLWMFOY1-2HHoK0uLE8TgfDGFIN2rmVZT-RNPjn9vNZThmNUDvJSzR1y8OGsfKk=","summary":[{"type":"summary_text","text":"**Planning extraction of grid operations 260-500**"}]},{"id":"fc_0ce5441d29b9f05d016a5b41c0fbf4819b81b818855d5ca6eb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading cell mutation semantics\",\"path\":\"crates/spt-term/src/screen.rs\",\"selector\":\"240-500\"}","call_id":"call_siFsUWmxBfFXHp4lNkV0i0l8","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41c0fc00819b8044e1a4e41fc257","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading RC setup and cleanup\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"750-870,1100-1370\"}","call_id":"call_xidwSyfc3e0Uu3g6mAsTOjSG","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41c0fc08819bb43dd75069d9244e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading RC reconnect and pump\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1390-1735,1740-2030\"}","call_id":"call_BCpoPBMW3NJUmtBkBBtzVogr","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41c0fc10819b865cb3bc7c0f6b59","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading serve terminal exits\",\"path\":\"crates/spt-daemon/src/attach.rs\",\"selector\":\"540-728\"}","call_id":"call_f5orIrgxrs7dXTDNuYBfyhd4","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41c0fc14819bb2da97fe43b4d805","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker disconnect sweep\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"3235-3510\"}","call_id":"call_k3MhmjWJ5bToC5p8L2PRjEos","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41c0fc1c819bac7478c6d236de7c","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding output transformations\",\"pattern\":\"decode_bytes|encode_bytes|data_b64|AttachRecord::Output|OutputEvent|output_envelope\",\"path\":\"crates/spt/src/rc.rs; crates/spt-daemon/src/attach.rs; crates/spt-daemon/src/brain.rs; crates/spt-daemon/src/msg.rs; crates/spt-daemon/src/broker.rs; crates/spt-net/src/net/attach.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_R1ySxe7weSPvSMdp0XpAWauA","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41c0fc20819b90c510cea2778c32","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding rc timeout constants\",\"pattern\":\"const .*TIMEOUT|FIRST_EVENT|RECONNECT|POLL_SLICE|STALL|grace|deadline\",\"path\":\"crates/spt/src/rc.rs; crates/spt-daemon/src/attach.rs; crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/conn.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_cozkvEQP2IDSg4zz1tIfEAw8","name":"grep"}]},"duration":10198.03330001235,"ttft":2053.133500009775,"contextSnapshot":{"promptTokens":118399,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_siFsUWmxBfFXHp4lNkV0i0l8|fc_0ce5441d29b9f05d016a5b41c0fbf4819b81b818855d5ca6eb","toolName":"read","startedAt":"2026-07-18T09:05:05.114Z","args":{"path":"crates/spt-term/src/screen.rs"},"intent":"Reading cell mutation semantics"},"id":"ab1d8782","parentId":"5dffd1e8","timestamp":"2026-07-18T09:05:05.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xidwSyfc3e0Uu3g6mAsTOjSG|fc_0ce5441d29b9f05d016a5b41c0fc00819b8044e1a4e41fc257","toolName":"read","startedAt":"2026-07-18T09:05:05.114Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading RC setup and cleanup"},"id":"20a364b1","parentId":"ab1d8782","timestamp":"2026-07-18T09:05:05.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BCpoPBMW3NJUmtBkBBtzVogr|fc_0ce5441d29b9f05d016a5b41c0fc08819bb43dd75069d9244e","toolName":"read","startedAt":"2026-07-18T09:05:05.114Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading RC reconnect and pump"},"id":"c362e09c","parentId":"20a364b1","timestamp":"2026-07-18T09:05:05.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_f5orIrgxrs7dXTDNuYBfyhd4|fc_0ce5441d29b9f05d016a5b41c0fc10819b865cb3bc7c0f6b59","toolName":"read","startedAt":"2026-07-18T09:05:05.114Z","args":{"path":"crates/spt-daemon/src/attach.rs"},"intent":"Reading serve terminal exits"},"id":"7ba8ee6d","parentId":"c362e09c","timestamp":"2026-07-18T09:05:05.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_k3MhmjWJ5bToC5p8L2PRjEos|fc_0ce5441d29b9f05d016a5b41c0fc14819bb2da97fe43b4d805","toolName":"read","startedAt":"2026-07-18T09:05:05.114Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker disconnect sweep"},"id":"28b782d9","parentId":"7ba8ee6d","timestamp":"2026-07-18T09:05:05.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_R1ySxe7weSPvSMdp0XpAWauA|fc_0ce5441d29b9f05d016a5b41c0fc1c819bac7478c6d236de7c","toolName":"grep","startedAt":"2026-07-18T09:05:05.114Z","args":{"path":"crates/spt/src/rc.rs; crates/spt-daemon/src/attach.rs; crates/spt-daemon/src/brain.rs; crates/spt-daemon/src/msg.rs; crates/spt-daemon/src/broker.rs; crates/spt-net/src/net/attach.rs"},"intent":"Finding output transformations"},"id":"1749c4e7","parentId":"28b782d9","timestamp":"2026-07-18T09:05:05.114Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_cozkvEQP2IDSg4zz1tIfEAw8|fc_0ce5441d29b9f05d016a5b41c0fc20819b90c510cea2778c32","toolName":"grep","startedAt":"2026-07-18T09:05:05.114Z","args":{"path":"crates/spt/src/rc.rs; crates/spt-daemon/src/attach.rs; crates/spt-daemon/src/broker.rs; crates/spt-daemon/src/conn.rs"},"intent":"Finding rc timeout constants"},"id":"0ec49c7c","parentId":"1749c4e7","timestamp":"2026-07-18T09:05:05.114Z"}
{"type":"message","id":"aa77ff52","parentId":"0ec49c7c","timestamp":"2026-07-18T09:05:05.153Z","message":{"role":"toolResult","toolCallId":"call_xidwSyfc3e0Uu3g6mAsTOjSG|fc_0ce5441d29b9f05d016a5b41c0fc00819b8044e1a4e41fc257","toolName":"read","content":[{"type":"text","text":"/// composition unit-tests without a console. (Win10 conhost defaults VT-output\n/// OFF; Win11 Windows Terminal has it on — the source of the #12 divergence.)\n// [impl->REQ-RC-WIN-VT-OUTPUT]\n#[cfg(windows)]\nfn with_vt_output(prior: u32) -> u32 {\n    use windows_sys::Win32::System::Console::{\n        ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING,\n    };\n    prior | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT\n}\n\n/// Enable VT-output on the real console and return the prior mode to restore.\n/// `None` when there is no real console (a piped stdout → `GetConsoleMode` fails)\n/// or the calls error — fail-safe: the byte pump is unchanged, so a redirected\n/// stdout keeps clean bytes for the e2e tests even if the interactive guard passed.\n// [impl->REQ-RC-WIN-VT-OUTPUT]\n#[cfg(windows)]\nfn enable_vt_output() -> Option<u32> {\n    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;\n    use windows_sys::Win32::System::Console::{\n        GetConsoleMode, GetStdHandle, SetConsoleMode, STD_OUTPUT_HANDLE,\n    };\n    unsafe {\n        let handle = GetStdHandle(STD_OUTPUT_HANDLE);\n        if handle.is_null() || handle == INVALID_HANDLE_VALUE {\n            return None;\n        }\n        let mut prior: u32 = 0;\n        if GetConsoleMode(handle, &mut prior) == 0 {\n            return None; // not a console (piped) — nothing to enable\n        }\n        if SetConsoleMode(handle, with_vt_output(prior)) == 0 {\n            return None;\n        }\n        Some(prior)\n    }\n}\n\n/// Restore a previously-captured OUTPUT console mode (#12).\n// [impl->REQ-RC-WIN-VT-OUTPUT]\n#[cfg(windows)]\nfn restore_out_mode(prior: u32) {\n    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;\n    use windows_sys::Win32::System::Console::{GetStdHandle, SetConsoleMode, STD_OUTPUT_HANDLE};\n    unsafe {\n        let handle = GetStdHandle(STD_OUTPUT_HANDLE);\n        if !handle.is_null() && handle != INVALID_HANDLE_VALUE {\n            let _ = SetConsoleMode(handle, prior);\n        }\n    }\n}\n\n/// Restore the terminal out of raw mode (and mouse capture) no matter how the\n/// pump returns.\nstruct RawGuard {\n    raw: bool,\n    /// The prior STD_OUTPUT_HANDLE console mode to restore on drop (#12,\n    /// REQ-RC-WIN-VT-OUTPUT) — `Some` only when we enabled VT-output on an\n    /// interactive Windows console.\n    #[cfg(windows)]\n    prior_out_mode: Option<u32>,\n}\nimpl RawGuard {\n    fn enable() -> Self {\n        let raw = crossterm::terminal::enable_raw_mode().is_ok();\n        #[cfg(windows)]\n        let mut prior_out_mode = None;\n        // Windows: capture the mouse so a RIGHT-CLICK reaches the app as\n        // `Event::Mouse` (disables console QuickEdit + enables ENABLE_MOUSE_INPUT)\n        // for the right-click paste gesture (REQ-RC-WIN-PASTE). Interactive console\n        // only — a piped stdin keeps a clean stdout for the e2e byte tests.\n        #[cfg(windows)]\n        if windows_mouse_wanted() {\n            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture);\n            // Enable VT-OUTPUT so the remote harness's ANSI escapes render instead\n            // of printing literally on a Win10 conhost console (#12,\n            // REQ-RC-WIN-VT-OUTPUT). Same interactive-console guard as the mouse\n            // capture; a piped stdout fails GetConsoleMode → no-op → clean e2e\n            // bytes. Prior mode captured for restore on drop.\n            prior_out_mode = enable_vt_output();\n        }\n        #[cfg(windows)]\n        {\n            RawGuard { raw, prior_out_mode }\n        }\n        #[cfg(not(windows))]\n        {\n            RawGuard { raw }\n        }\n    }\n}\nimpl Drop for RawGuard {\n    fn drop(&mut self) {\n        #[cfg(windows)]\n        if windows_mouse_wanted() {\n            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);\n        }\n        // Restore the prior VT-output console mode (#12, REQ-RC-WIN-VT-OUTPUT).\n        #[cfg(windows)]\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…\nfn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n…\n    std::thread::spawn(move || {\n…\n    });\n}\n\n/// Attach a local terminal to the broker-held session for `endpoint_id` under\n/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is\n/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,\n/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and\n/// takes control. Returns when the session exits, the operator detaches, or an\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n            // 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…\n}"}],"details":{"displayContent":{"text":"/// composition unit-tests without a console. (Win10 conhost defaults VT-output\n/// OFF; Win11 Windows Terminal has it on — the source of the #12 divergence.)\n// [impl->REQ-RC-WIN-VT-OUTPUT]\n#[cfg(windows)]\nfn with_vt_output(prior: u32) -> u32 {\n    use windows_sys::Win32::System::Console::{\n        ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING,\n    };\n    prior | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT\n}\n\n/// Enable VT-output on the real console and return the prior mode to restore.\n/// `None` when there is no real console (a piped stdout → `GetConsoleMode` fails)\n/// or the calls error — fail-safe: the byte pump is unchanged, so a redirected\n/// stdout keeps clean bytes for the e2e tests even if the interactive guard passed.\n// [impl->REQ-RC-WIN-VT-OUTPUT]\n#[cfg(windows)]\nfn enable_vt_output() -> Option<u32> {\n    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;\n    use windows_sys::Win32::System::Console::{\n        GetConsoleMode, GetStdHandle, SetConsoleMode, STD_OUTPUT_HANDLE,\n    };\n    unsafe {\n        let handle = GetStdHandle(STD_OUTPUT_HANDLE);\n        if handle.is_null() || handle == INVALID_HANDLE_VALUE {\n            return None;\n        }\n        let mut prior: u32 = 0;\n        if GetConsoleMode(handle, &mut prior) == 0 {\n            return None; // not a console (piped) — nothing to enable\n        }\n        if SetConsoleMode(handle, with_vt_output(prior)) == 0 {\n            return None;\n        }\n        Some(prior)\n    }\n}\n\n/// Restore a previously-captured OUTPUT console mode (#12).\n// [impl->REQ-RC-WIN-VT-OUTPUT]\n#[cfg(windows)]\nfn restore_out_mode(prior: u32) {\n    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;\n    use windows_sys::Win32::System::Console::{GetStdHandle, SetConsoleMode, STD_OUTPUT_HANDLE};\n    unsafe {\n        let handle = GetStdHandle(STD_OUTPUT_HANDLE);\n        if !handle.is_null() && handle != INVALID_HANDLE_VALUE {\n            let _ = SetConsoleMode(handle, prior);\n        }\n    }\n}\n\n/// Restore the terminal out of raw mode (and mouse capture) no matter how the\n/// pump returns.\nstruct RawGuard {\n    raw: bool,\n    /// The prior STD_OUTPUT_HANDLE console mode to restore on drop (#12,\n    /// REQ-RC-WIN-VT-OUTPUT) — `Some` only when we enabled VT-output on an\n    /// interactive Windows console.\n    #[cfg(windows)]\n    prior_out_mode: Option<u32>,\n}\nimpl RawGuard {\n    fn enable() -> Self {\n        let raw = crossterm::terminal::enable_raw_mode().is_ok();\n        #[cfg(windows)]\n        let mut prior_out_mode = None;\n        // Windows: capture the mouse so a RIGHT-CLICK reaches the app as\n        // `Event::Mouse` (disables console QuickEdit + enables ENABLE_MOUSE_INPUT)\n        // for the right-click paste gesture (REQ-RC-WIN-PASTE). Interactive console\n        // only — a piped stdin keeps a clean stdout for the e2e byte tests.\n        #[cfg(windows)]\n        if windows_mouse_wanted() {\n            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture);\n            // Enable VT-OUTPUT so the remote harness's ANSI escapes render instead\n            // of printing literally on a Win10 conhost console (#12,\n            // REQ-RC-WIN-VT-OUTPUT). Same interactive-console guard as the mouse\n            // capture; a piped stdout fails GetConsoleMode → no-op → clean e2e\n            // bytes. Prior mode captured for restore on drop.\n            prior_out_mode = enable_vt_output();\n        }\n        #[cfg(windows)]\n        {\n            RawGuard { raw, prior_out_mode }\n        }\n        #[cfg(not(windows))]\n        {\n            RawGuard { raw }\n        }\n    }\n}\nimpl Drop for RawGuard {\n    fn drop(&mut self) {\n        #[cfg(windows)]\n        if windows_mouse_wanted() {\n            let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);\n        }\n        // Restore the prior VT-output console mode (#12, REQ-RC-WIN-VT-OUTPUT).\n        #[cfg(windows)]\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…\nfn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {\n…\n    std::thread::spawn(move || {\n…\n    });\n}\n\n/// Attach a local terminal to the broker-held session for `endpoint_id` under\n/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is\n/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,\n/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and\n/// takes control. Returns when the session exits, the operator detaches, or an\n/// error occurs.\n// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\npub fn run_attach(endpoint_id: &str, intent: AttachIntent) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, false)\n}\n\n/// Like [`run_attach`], but for a caller that has ALREADY confirmed a live broker\n/// session (e.g. `cmd_endpoint_run` after [`SessionProbe::has_session`] via\n/// await_endpoint_session). Skips the (a) offline STATUS short-circuit: a\n/// RESUMING perch is still stamped `offline` in its pre-bind window (the W2\n/// skeleton-write short-circuits an existing perch, so resume gets no UNBOUND\n/// stamp), yet the confirmed live session IS attachable — \"attach to a live\n/// broker session regardless of perch status\" (REQ-ENDPOINT-UNBOUND-ATTACH).\n/// The (b) bounded first-event backstop + (c) BrokerGone EOF still guard a\n/// session that dies after confirmation, so this is not a blank-hang reopening.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn run_attach_session_confirmed(\n    endpoint_id: &str,\n    intent: AttachIntent,\n) -> Result<(), String> {\n    run_attach_inner(endpoint_id, intent, true)\n}\n\nfn run_attach_inner(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    session_confirmed: bool,\n) -> Result<(), String> {\n    let view = intent == AttachIntent::Viewer;\n    // Busy-refuse rendered CLIENT-SIDE (REQ-RCVIEW-1): a plain `Control` attach to\n    // an endpoint a DIFFERENT node already controls gets actionable guidance here\n    // (the broker would also refuse on the wire — this is the friendly path). A\n    // `--view` / `--take` bypasses it (watching coexists; taking displaces).\n    if intent == AttachIntent::Control {\n        if let Some(node) = current_driver(endpoint_id) {\n            println!(\"'{endpoint_id}' is controlled by {node}.\");\n            println!(\"  spt rc {endpoint_id} --view   to watch read-only\");\n            println!(\"  spt rc {endpoint_id} --take   to take control (kicks {node})\");\n            return Ok(());\n        }\n    }\n    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n    // `status=offline` is not attachable — short-circuit with clean guidance\n    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n    // instead of the broker dance that would block on a dead session (the\n    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n    // spt-hosted perch offline, so this catches the common case before any IPC. An\n    // ABSENT or status-less perch falls through to the broker path, which already\n    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n    // is NOT used here: it returns true for an absent info.json by interim parity.)\n    //\n    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n    // NOT on STATUS_ONLINE (REQ-ENDPOINT-UNBOUND-ATTACH). A STATUS_UNBOUND skeleton\n    // (session spawned, perch not yet bound) is `!= offline`, so it falls through\n    // here and attaches via the live-session resolve below — the headless /\n    // clear-a-bind-prompt path. `offline` is stamped only on session DEATH, so it\n    // never co-occurs with a live session.\n    //\n    // KNOWN BOUNDARY (doyle, W4): a RESUMING perch is still `offline` during its\n    // pre-bind window — the W2 skeleton-write short-circuits on an existing perch,\n    // so resume gets NO UNBOUND stamp. A standalone `spt rc <id>` against it hits\n    // this failfast (\"offline — start it\") rather than waiting for the bind, and\n    // retries fine once the bind flips the perch online. The `endpoint run\n    // --attach` path does NOT hit it: it arrives via run_attach_session_confirmed\n    // (session already proven live by await_endpoint_session), so the gate is\n    // SKIPPED — it attaches to the confirmed live session even while the perch is\n    // still stamped offline mid-rebringup. Accepted, not contorted around.\n    // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n    let perch_path =\n        spt_store::perch::resolve_perch_path(endpoint_id, spt_store::perch::ParentHint::Infer);\n    if !session_confirmed\n        && spt_store::info::read_info(&perch_path)\n            .and_then(|i| i.status)\n            .as_deref()\n            == Some(spt_store::liveness::STATUS_OFFLINE)\n    {\n        println!(\"Endpoint '{endpoint_id}' is offline — nothing to attach to.\");\n        println!(\"  spt endpoint run --adapter <adapter> --id {endpoint_id}   to start it\");\n        return Ok(());\n    }\n    // Establish the attach (broker conn → session resolve → dial → attach-open →\n    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n    // keeps today's clean-exit copy on a no-target miss.\n    let est = match establish_attach(endpoint_id, intent) {\n        Ok(est) => est,\n        Err(EstablishFail::NoTarget(msg)) => {\n            println!(\"{msg}\");\n            return Ok(());\n        }\n        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n        // rc won't birth one. Plain operator language — what's wrong, what to do —\n        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n        // [impl->REQ-RC-RECONNECT-TRUTH]\n        Err(EstablishFail::DaemonDown) => {\n            println!(\n                \"The spt daemon isn't running — nothing to attach to. \\\n                 Start it with `spt daemon start`, then `spt rc {endpoint_id}`.\"\n            );\n            return Ok(());\n        }\n        Err(EstablishFail::Error(e)) => return Err(e),\n    };\n    let mut remote_node: Option<String> = est.remote_node.clone();\n\n    let _raw = RawGuard::enable();\n    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the\n    // pump and read by the Windows stdin reader to gate scroll forwarding\n    // (REQ-RC-MOUSE-FORWARD).\n    let mouse_mode = std::sync::Arc::new(MouseMode::default());\n    let rx = if view {\n        None\n    } else {\n        let (tx, rx) = mpsc::channel();\n        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));\n        Some(rx)\n    };\n\n    // Controller status row (REQ-RC-IDENTITY): the operator-side identity banner\n    // on the real terminal's reserved row 1 — controller only (a viewer's viewport\n    // is read-only and may letterbox, so it owns no row). Resolved ONCE here.\n    // [impl->REQ-RC-IDMARKER-DISABLE]\n    let mut status = if status_row_active(view) {\n        let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n        Some(StatusRow {\n            text: resolve_identity_line(endpoint_id),\n            rows,\n            cols,\n        })\n    } else {\n        None\n    };\n\n    let mut stdout = std::io::stdout();\n    let outcome = attach_viewport(\n        endpoint_id,\n        intent,\n        view,\n        est,\n        rx.as_ref(),\n        &mouse_mode,\n        status.as_mut(),\n        &mut remote_node,\n        &mut stdout,\n    );\n\n    // Tear the reserved status row down (reset scroll region + clear row 1) BEFORE\n    // leaving raw mode, on every pump-exit path (REQ-RC-IDENTITY).\n    if status.is_some() {\n        let _ = stdout.write_all(&StatusRow::restore_bytes());\n        let _ = stdout.flush();\n    }\n    // Out of raw mode (guard drops after) before the parting line.\n    drop(_raw);\n    match outcome {\n        Ok(PumpEnd::Exited(code)) => {\n            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Detached) => {\n            let _ = writeln!(stdout, \"\\r\\n[detached — '{endpoint_id}' still running]\");\n            Ok(())\n        }\n        Ok(PumpEnd::Displaced(by)) => {\n            // The loud kick notice (REQ-KICK-1): a different operator took control.\n            let _ = writeln!(\n                stdout,\n                \"\\r\\n[displaced — '{endpoint_id}' was taken over by {by}]\"\n            );\n            Ok(())\n        }\n        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n            // successful re-attach. Plain operator language (Q4 UX rule): what\n            // happened, what it means, what to do next — never op/read-err lingo.\n            if daemon_down {\n                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n                // went down and never came back — rc did NOT resurrect it. Name that\n                // plainly so the operator restarts the daemon rather than guessing.\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[session '{endpoint_id}' lost — the spt daemon is down and \\\n                     didn't come back within {}s. rc won't auto-start it. Bring it \\\n                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            } else {\n                let _ = writeln!(\n                    stdout,\n                    \"\\r\\n[{detail} and reconnecting to '{endpoint_id}' didn't succeed \\\n                     within {}s — the session may still be running. Try `spt rc \\\n                     {endpoint_id}` again once the node/daemon is back]\",\n                    RECONNECT_WINDOW.as_secs()\n                );\n            }\n            Ok(())\n        }\n        // Reconnectable ends never escape the loop (they enter the retry arm and\n        // resolve to a re-attach, a Detached, or a ReconnectGaveUp) — kept explicit\n        // so a future PumpEnd variant still forces a decision here.\n        Ok(PumpEnd::BrokerGone) | Ok(PumpEnd::Severed) => {\n            unreachable!(\"reconnect-class ends are consumed by the retry loop\")\n        }\n        Ok(PumpEnd::Stalled) => {\n            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced\n            // no output at all — a dead/wedged session, not a slow one. A clear\n            // 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…\n}","startLine":750,"lineNumbers":[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,null,1043,null,1046,null,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,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,null,1376]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"}}},"isError":false,"timestamp":1784365505153}}
{"type":"message","id":"4b4071d4","parentId":"aa77ff52","timestamp":"2026-07-18T09:05:05.153Z","message":{"role":"toolResult","toolCallId":"call_BCpoPBMW3NJUmtBkBBtzVogr|fc_0ce5441d29b9f05d016a5b41c0fc08819bb43dd75069d9244e","toolName":"read","content":[{"type":"text","text":"enum EstablishFail {\n…\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n                        })?\n                        .conn_id,\n                    // Remote: re-resolve + re-dial the owning node.\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\n                        _ => {\n                            return Err(std::io::Error::other(format!(\n                                \"'{endpoint_id}' is no longer reachable\"\n                            )))\n                        }\n                    },\n                },\n            };\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n    // Subscribe our side of the stream so the target's `Output` records surface\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\n    brain\n        .net_stream_subscribe(stream_id, 0)\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\n    Ok(Established {\n        brain,\n        ops,\n        stream_id,\n        remote_node,\n    })\n}\n\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\n/// inside a bounded window; every other end is FINAL and returns to the\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\n/// made TRUE here, not edited down (triage F-3).\n///\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\n/// construction/teardown, the parting copy. This fn only needs an established\n/// attach + an `out` sink, so the two-host rig can drive the REAL loop headless\n/// (`out` = a capture buffer) — no behavior change for the interactive path.\n/// `remote_node` is caller-owned so the parting copy names the LAST node the\n/// viewport was attached to (a reconnect can re-resolve it).\n#[allow(clippy::too_many_arguments)]\nfn attach_viewport(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    view: bool,\n    mut est: Established,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n    remote_node: &mut Option<String>,\n    out: &mut impl std::io::Write,\n) -> Result<PumpEnd, String> {\n    loop {\n        // Controller: send the INITIAL terminal size so the PTY matches from the\n        // start (REQ-RCVIEW-1). Best-effort — a failure just leaves the default\n        // size. When the status row is active the harness PTY gets rows-1 (we own\n        // row 1), never 0 (REQ-RC-IDENTITY). Re-sent per establish: the new\n        // viewport's PTY must match our CURRENT terminal, not the original one.\n        if !view {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                let pty_rows = if status.is_some() {\n                    rows.saturating_sub(1).max(1)\n                } else {\n                    rows\n                };\n                let _ = send_attach_resize(&mut est.brain, est.stream_id, pty_rows, cols);\n            }\n        }\n\n        let mut decoder = AttachDecoder::new();\n        let end = pump(\n            &mut est.brain,\n            est.stream_id,\n            &est.ops,\n            rx,\n            &mut decoder,\n            out,\n            !view, // controller drives resize\n            mouse_mode,\n            status.as_deref_mut(),\n        );\n\n        // Final ends (incl. hard errors) exit the loop to the parting copy.\n        let severed_kind = match &end {\n            Ok(e) if reconnect_class(e) == EndClass::Reconnect => match e {\n                PumpEnd::BrokerGone => \"the daemon connection dropped\",\n                _ => \"the connection was severed\",\n            },\n            _ => return end,\n        };\n\n        // Reconnect: full-screen centered banner (operator language — the internal\n        // detail stays off the terminal per the Q4 UX rule), then re-drive\n        // establish_attach inside the bounded window. A detach keypress mid-window\n        // aborts the retry loop honestly. The banner repaints each iteration with a\n        // live COUNTDOWN (REQ-RC-RECONNECT-TRUTH) so the operator sees rc is bounded\n        // and working, never the old apparent freeze at a static \"Reconnecting…\".\n        // establish_attach is CONNECT-ONLY: a down daemon yields DaemonDown here and\n        // is retried (the operator may bring it back) WITHOUT any WMI resurrection.\n        let target = remote_node.clone().unwrap_or_else(|| \"local daemon\".to_string());\n        let retry_started = Instant::now();\n        let reestablished = loop {\n            let elapsed = retry_started.elapsed();\n            if reconnect_expired(elapsed) {\n                break None;\n            }\n            let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n            let remaining = reconnect_remaining_secs(elapsed);\n            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\n            let _ = out.flush();\n            // Operator gives up mid-reconnect: honor the detach keys.\n            if let Some(rx) = rx {\n                let mut detached = false;\n                while let Ok(msg) = rx.try_recv() {\n                    if matches!(msg, StdinMsg::Detach) {\n                        detached = true;\n                    }\n                }\n                if detached {\n                    break Some(Err(())); // detach sentinel\n                }\n            }\n            match establish_attach(endpoint_id, intent) {\n                Ok(next) => break Some(Ok(next)),\n                // Every fail shape retries until the window expires — the node/daemon\n                // may still be coming back. DaemonDown included: rc waits for the\n                // operator to restart it, it does NOT auto-start one.\n                Err(_) => std::thread::sleep(RECONNECT_PAUSE),\n            }\n        };\n        match reestablished {\n            Some(Ok(next)) => {\n                *remote_node = next.remote_node.clone();\n                est = next;\n                // Loop: fresh resize + pump. The re-serve replays the output ring,\n                // repainting the screen the banner cleared.\n            }\n            Some(Err(())) => return Ok(PumpEnd::Detached),\n            None => {\n                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\n                // daemon (only knowable for a LOCAL target) gets the loud \"session\n                // lost — daemon down\" copy; anything else keeps the generic\n                // didn't-reconnect copy. Remote severs never probe local daemon state.\n                let daemon_down =\n                    remote_node.is_none() && !spt_daemon::daemon::is_running();\n                return Ok(PumpEnd::ReconnectGaveUp {\n                    detail: severed_kind.to_string(),\n                    daemon_down,\n                });\n            }\n        }\n    }\n}\n\n/// How long the B-3 reconnect loop keeps re-driving `establish_attach` after a\n/// severed viewport before the plain-language give-up (REQ-RC-RECONNECT).\n/// Generous: a daemon bounce (`spt daemon stop` + the next auto-start) and a\n/// node's broker restart both land well inside it.\nconst RECONNECT_WINDOW: Duration = Duration::from_secs(30);\n/// The pause between reconnect attempts (each attempt already carries its own\n/// bounded setup deadlines — this just keeps the loop from hammering).\nconst RECONNECT_PAUSE: Duration = Duration::from_secs(1);\n\n/// The give-up decision, pulled out so it unit-tests without a 30s wait.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_expired(elapsed: Duration) -> bool {\n    elapsed >= RECONNECT_WINDOW\n}\n\n/// Seconds still left in the reconnect window, for the banner countdown\n/// (REQ-RC-RECONNECT-TRUTH). CEIL so the display reads a full `RECONNECT_WINDOW`s\n/// at the start and never flashes `0s` while the loop is still trying (0 only once\n/// the window is fully spent, where the caller has already broken to give-up).\n// [impl->REQ-RC-RECONNECT-TRUTH]\nfn reconnect_remaining_secs(elapsed: Duration) -> u64 {\n    let left = RECONNECT_WINDOW.saturating_sub(elapsed);\n    // ceil(left / 1s) via integer millis — 1..=WINDOW while any time remains, 0 at/past.\n    (left.as_millis() as u64).div_ceil(1000)\n}\n\n/// Which pump ends re-drive the viewport (B-3) vs. exit with parting copy.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum EndClass {\n    Final,\n    Reconnect,\n}\n\n/// A severed transport is RECONNECTABLE: the broker-conn EOF class (broker\n/// bounce) and the serve-side EOF after rendered output (remote conn drop).\n/// Everything else — session exit, operator detach, displacement, the stall\n/// backstop, an attach refuse — is FINAL: re-driving those would re-attach to a\n/// session the operator (or the session itself) deliberately ended.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_class(end: &PumpEnd) -> EndClass {\n    match end {\n        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n        PumpEnd::Exited(_)\n        | PumpEnd::Detached\n        | PumpEnd::Displaced(_)\n        | PumpEnd::Stalled\n        | PumpEnd::NoLiveSession\n        | PumpEnd::ReconnectGaveUp { .. } => EndClass::Final,\n    }\n}\n\n/// Classify a serve-side stream EOF: rendered output first ⇒ the conn was\n/// SEVERED under a live viewport (reconnect); nothing ever rendered ⇒ the serve\n/// REFUSED the attach (no live session / gate deny — the cross-node D6\n/// stale-row case), honest \"no live session\" instead of a false \"detached\"\n/// (#4, REQ-RC-CROSS-NODE-ATTACH).\n// [impl->REQ-RC-RECONNECT]\nfn serve_eof_end(rendered_any: bool) -> PumpEnd {\n    if rendered_any {\n        PumpEnd::Severed\n    } else {\n        PumpEnd::NoLiveSession\n    }\n}\n\n/// The full-screen reconnect banner (B-3): clear + home, then the operator-language\n/// line centered on the terminal. Pure byte-emit (like [`StatusRow`]) so the exact\n/// VT shape unit-tests directly; the caller writes the bytes. The text is plain\n/// operator language (Q4 UX rule) — the internal sever detail never paints here.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_banner_bytes(rows: u16, cols: u16, target: &str, remaining_secs: u64) -> Vec<u8> {\n    // Visible countdown (REQ-RC-RECONNECT-TRUTH): the remaining-seconds suffix turns\n    // the old static banner into a live \"rc is working, bounded\" signal — the operator\n    // never mistakes a bounded retry for the historical freeze-forever.\n    let text = format!(\"Reconnecting to {target}… {remaining_secs}s\");\n    let row = (rows / 2).max(1);\n…\n    let mut out = Vec::new();\n    out.extend_from_slice(b\"\\x1b[2J\\x1b[H\");\n    out.extend_from_slice(format!(\"\\x1b[{row};{col}H\").as_bytes());\n    out.extend_from_slice(text.as_bytes());\n    out\n}\n\n#[derive(Debug)]\nenum PumpEnd {\n    Exited(Option<i32>),\n    Detached,\n    /// A different operator took control (REQ-KICK-1); carries the taker's node.\n    Displaced(String),\n    /// The broker stream was severed mid-session (broker bounce / hang-up — e.g. a\n    /// `spt daemon stop`). Recoverable, not a crash: the broker is the\n    /// daemon-lifetime anchor and returns on the next `spt api` call, so this\n    /// enters the B-3 reconnect loop (REQ-RC-RECONNECT) rather than exiting.\n    BrokerGone,\n    /// The serve side finished the stream AFTER rendering output — the remote\n    /// conn dropped (node bounce, network cut), NOT an operator detach (that\n    /// returns via the stdin `Detach` path directly) and NOT a session end (an\n    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\n    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\n    /// re-drives. [impl->REQ-RC-RECONNECT]\n    Severed,\n    /// B-3 terminal state: the reconnect window expired without a successful\n    /// re-attach. `detail` is the operator-language cause of the ORIGINAL sever\n    /// (Q4 UX rule — never read-error lingo). `daemon_down` records whether the\n    /// local daemon was still down at give-up (REQ-RC-RECONNECT-TRUTH) — it\n    /// selects the loud \"session lost — daemon down\" copy over the generic\n    /// didn't-reconnect copy. [impl->REQ-RC-RECONNECT] [impl->REQ-RC-RECONNECT-TRUTH]\n    ReconnectGaveUp { detail: String, daemon_down: bool },\n    /// The attach produced NO event whatsoever within the generous first-event\n    /// backstop window (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a healthy session —\n    /// even one still painting / mid-init — replays its buffered output (or at\n    /// least a Subscribed/Size frame) promptly, so a totally silent stream past\n    /// this bound is a dead/wedged session, not a slow one. Surfaces a clear\n    /// message instead of the historical infinite blank. The bound is deliberately\n    /// LONG (only ever bites a pathological hang; the broker-EOF-on-dead-child path\n    /// and the status-gate catch the ordinary dead cases first).\n    Stalled,\n    /// The serve side finished the stream having produced NO output — i.e. it\n    /// refused the attach (no live session for the endpoint, or the access gate\n    /// denied us). On the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH) this is\n    /// the D6 stale-row case: the registry routed us to a node that no longer has\n    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\n    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\n    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    NoLiveSession,\n}\n\n/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST\n/// path b): if NO broker event arrives within this window after attach, the\n/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\n/// session (empty OutputLog, still painting) MUST pass, so this only catches a\n/// true hang, never the working fresh-attach.\nconst FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\n\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\n/// stalled iff NO broker event has ever arrived AND the generous grace has\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\n}\n\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ReadDisposition {\n    /// Transient poll-slice timeout — loop and poll again.\n    Retry,\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n    /// gracefully.\n    BrokerGone,\n    /// A genuine read failure — propagate.\n    Fatal,\n}\n\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\n/// family the v0.9.1 seed fix handles); anything else is fatal.\n// [impl->REQ-HAZARD-RC-EOF]\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\n    use std::io::ErrorKind::*;\n    match kind {\n        WouldBlock | TimedOut => ReadDisposition::Retry,\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n            ReadDisposition::BrokerGone\n        }\n        _ => ReadDisposition::Fatal,\n    }\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n) -> Result<PumpEnd, String> {\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n    // scanner is pump-local (single producer) and survives a sequence split across\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n    let mut mouse_scanner = MouseModeScanner::default();\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\n    let mut reassert_scanner = ReassertScanner::default();\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\n    // Controller-only (the caller passes `Some` only for a control attach).\n    if let Some(status) = status.as_deref_mut() {\n        stdout\n            .write_all(&status.assert_bytes())\n            .map_err(|e| format!(\"status assert: {e}\"))?;\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n    }\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n    // (the session outlives the viewport). Best-effort: an already-finished or\n    // exited stream send is harmless to attempt.\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\n        PumpEnd::Detached\n    }\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n    // drop any `seq` already rendered (the operator render cursor, matching the\n    // local `OutputLog` replay discipline).\n    let mut cursor = 0u64;\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n    // each slice and forward a Resize when it changes (cross-platform without\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n    // raw-stdin reader). `None` until the first read.\n    let mut last_size: Option<(u16, u16)> = None;\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n    // as a one-line indicator when it differs from our own terminal.\n    let mut pty_size: Option<(u16, u16)> = None;\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\n    // rather than an infinite blank. Any broker event clears it.\n    let started = Instant::now();\n    let mut seen_any = false;\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n    // Any live session replays its buffered ring on attach, so a real attach sets\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n    let mut rendered_any = false;\n    loop {\n        // ── controller window-change → resize ──────────────────────────────\n        if controller {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                if last_size != Some((rows, cols)) {\n                    last_size = Some((rows, cols));\n                    match status.as_deref_mut() {\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\n                        // for the banner), and we re-assert the margin + repaint on\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\n                        Some(status) => {\n                            status.update_size(rows, cols);\n                            let pty_rows = rows.saturating_sub(1).max(1);\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n                                let _ = stdout.flush();\n                            }\n                        }\n                        None => {\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\n                        }\n                    }\n                }\n            }\n        }\n\n        // ── stdin → session ────────────────────────────────────────────────\n        if let Some(rx) = rx {\n            loop {\n                match rx.try_recv() {\n                    Ok(StdinMsg::Bytes(bytes)) => {\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\n                            .map_err(|e| format!(\"send input: {e}\"))?;\n                    }\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n                    Err(mpsc::TryRecvError::Empty) => break,\n                    Err(mpsc::TryRecvError::Disconnected) => {\n                        return Ok(detach(brain, stream_id))\n                    }\n                }\n            }\n        }\n\n        // ── session → terminal ─────────────────────────────────────────────\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n            Ok(ev) => ev,\n            Err(e) => match classify_read_err(e.kind()) {\n                // A poll-slice timeout: no event this slice. Before looping, the\n                // first-event backstop — if NOTHING has ever arrived past the\n                // generous grace, the attach is dead/wedged (path b).\n                ReadDisposition::Retry => {\n                    if first_event_stalled(seen_any, started.elapsed()) {\n                        return Ok(PumpEnd::Stalled);\n                    }\n                    continue;\n                }\n                // A severed broker stream (broker bounce / hang-up) — surface it\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n            },\n        };\n        seen_any = true; // any broker event proves the attach stream is live\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n                            // Re-assert the reserved row if the harness output just\n                            // destroyed our scroll region — alt-screen enter or a\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n                            // margin is gone (alt-screen has its own full-screen\n                            // margins; a DECSTBM reset clears ours), so re-set the\n                            // MARGIN + repaint (assert_bytes), not just the text —\n                            // else the harness scrolls back over row 1. Scanned on the\n                            // SAME bytes we rendered, after writing them.\n                            if let Some(status) = status.as_deref_mut() {\n                                if reassert_scanner.feed(&bytes) {\n                                    stdout\n                                        .write_all(&status.assert_bytes())\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n                                }\n                            }\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n                        }\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n                        AttachRecord::Displaced { by } => {\n                            return Ok(PumpEnd::Displaced(by));\n                        }\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\n                        // show a one-line indicator when our terminal differs from\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\n                        // a grid model, deferred; the honest size cue renders here).\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\n                            pty_size = Some((rows, cols));\n                            match crossterm::terminal::size() {\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\n                                    let _ = write!(\n                                        stdout,\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n                                    );\n                                    let _ = stdout.flush();\n                                }\n                                _ => {}\n                            }\n                        }\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                return Ok(serve_eof_end(rendered_any));\n            }\n            BrokerEvent::Error { message } => return Err(message),\n            _ => {} // acks + unrelated events: not ours.\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n…\n}"}],"details":{"displayContent":{"text":"enum EstablishFail {\n…\n    DaemonDown,\n    /// A setup error (broker connect / dial / attach-open / subscribe).\n    Error(String),\n}\n\n/// Drive the full attach-establish sequence: ensure daemon → broker conn →\n/// session resolve (LOCAL first, cross-node on miss) → dial → attach-open (with\n/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n///\n/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n/// is not supported\"), so local attach cannot ride a self-dialed conn; instead\n/// the broker wires an in-process loopback conn whose streams surface as the\n/// SAME net-stream events the cross-node QUIC path produces. From\n/// `request_attach` down this is byte-identical to a cross-node attach — the\n/// one-pump invariant lives at the transport leaf inside the broker, never here.\n/// The loopback peer's origin is this node's own id, so the target's serve gates\n/// same-node.\n///\n/// Transport for a REMOTE attach: the registry-resolved node, seed-dialed over\n/// QUIC (mirrors the wansend resolve→seed_first_addr→net_dial leg). The owning\n/// node's serve is ALREADY transport-agnostic — a QUIC-arriving Attach serves\n/// identically to loopback — so from here down the two legs converge. Honest\n/// failure on every resolve/dial miss: a clean message, never a false attach or\n/// a hang (D2/D6).\n// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\nfn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n    // launcher. It must NEVER auto-start the broker — the historical\n    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n    // and, in the reconnect loop, re-birthed the very daemon the operator was\n    // tearing down. A daemon-down probe returns `DaemonDown` so the caller exits\n    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n    // broker cold-connect below is already deadline-bounded, so a down daemon\n    // never hangs.\n    // [impl->REQ-RC-RECONNECT-TRUTH]\n    if !spt_daemon::daemon::is_running() {\n        return Err(EstablishFail::DaemonDown);\n    }\n    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n    // pipes), so the single-threaded pump interleaves stdin + broker events\n    // without wedging. The generous io_timeout bounds the setup calls (dial,\n    // sessions, attach-open ack); the pump itself passes its own short slice.\n    let mut brain = Brain::cold_start_pump(\n        &spt_daemon::endpoint::broker_socket_name(),\n        now_ms(),\n        Duration::from_secs(10),\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"broker connect: {e}\")))?;\n\n    let ops = OpMinter::new();\n    // Resolve the endpoint's live session. LOCAL first (the broker's own session\n    // table); on a MISS, the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    // resolve the OWNING node from the registry and dial it, letting THAT node\n    // resolve endpoint→its session server-side.\n    let local_session = resolve_session(&mut brain, endpoint_id)\n        .map_err(|e| EstablishFail::Error(format!(\"list sessions: {e}\")))?;\n    let mut remote_node: Option<String> = None;\n    let (conn_id, session_id, req_endpoint): (u64, u64, Option<&str>) = match local_session {\n        Some(sid) => {\n            let dialed = brain\n                .net_dial_loopback()\n                .map_err(|e| EstablishFail::Error(format!(\"loopback dial: {e}\")))?;\n            (dialed.conn_id, sid, None)\n        }\n        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n                remote_node = Some(node);\n                // session_id is a placeholder the owning node ignores — it\n                // resolves the endpoint to its OWN session id server-side.\n                (conn_id, 0, Some(endpoint_id))\n            }\n            crate::wansend::OwnerDial::NotFound => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"No live session for '{endpoint_id}' — not running on this node \\\n                     or any visible subnet.\"\n                )))\n            }\n            crate::wansend::OwnerDial::Ambiguous(msg) => {\n                return Err(EstablishFail::NoTarget(msg))\n            }\n            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n                return Err(EstablishFail::NoTarget(format!(\n                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n                )))\n            }\n        },\n    };\n    // A-4b (REQ-OPID-TRACING-RETRY): the attach stream-open is a tracing-only op.\n    // On the broker-restart no-longer-held reply, re-mint a FRESH `rc` op and open\n    // ONCE more. The first attempt uses the conn already established above (happy\n    // path byte-identical); the one retry RE-DIALS inside `run` — a broker restart\n    // drops the CONN too, not just the stream, so opening against the dead conn\n    // would surface a different error instead of self-healing. A 2nd no-longer-held\n    // surfaces the helper's F-1 public string via the map_err below (never op lingo).\n    let mut established: Option<u64> = Some(conn_id);\n    let stream_id = spt_daemon::with_tracing_retry(\n        // Operator attach open — the rc OpMinter is the counter (ADR-0034 `rc` tag).\n        || Ok(MintedOp::new(Minter::Rc, ops.next())),\n        |op| {\n            let cid = match established.take() {\n                Some(c) => c,\n                None => match local_session {\n                    // Local: the broker's in-process loopback singleton (re-mint/reuse).\n                    Some(_) => brain\n                        .net_dial_loopback()\n                        .map_err(|e| {\n                            std::io::Error::other(format!(\"loopback re-dial: {e}\"))\n                        })?\n                        .conn_id,\n                    // Remote: re-resolve + re-dial the owning node.\n                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\n                        _ => {\n                            return Err(std::io::Error::other(format!(\n                                \"'{endpoint_id}' is no longer reachable\"\n                            )))\n                        }\n                    },\n                },\n            };\n            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n        },\n    )\n    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n    // Subscribe our side of the stream so the target's `Output` records surface\n    // as `NetStreamData` events (the open + Request do not auto-subscribe).\n    brain\n        .net_stream_subscribe(stream_id, 0)\n        .map_err(|e| EstablishFail::Error(format!(\"stream subscribe: {e}\")))?;\n    Ok(Established {\n        brain,\n        ops,\n        stream_id,\n        remote_node,\n    })\n}\n\n/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\n/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\n/// A severed viewport (broker bounce, remote conn drop — the reconnectable\n/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\n/// inside a bounded window; every other end is FINAL and returns to the\n/// caller's parting copy. The FAULT-MATRIX row-9 promise (\"rc reconnects\") is\n/// made TRUE here, not edited down (triage F-3).\n///\n/// The SEAM (doyle rig ruling #4): everything terminal-owning stays at the\n/// interactive caller — raw-mode guard, stdin-reader spawn, StatusRow\n/// construction/teardown, the parting copy. This fn only needs an established\n/// attach + an `out` sink, so the two-host rig can drive the REAL loop headless\n/// (`out` = a capture buffer) — no behavior change for the interactive path.\n/// `remote_node` is caller-owned so the parting copy names the LAST node the\n/// viewport was attached to (a reconnect can re-resolve it).\n#[allow(clippy::too_many_arguments)]\nfn attach_viewport(\n    endpoint_id: &str,\n    intent: AttachIntent,\n    view: bool,\n    mut est: Established,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n    remote_node: &mut Option<String>,\n    out: &mut impl std::io::Write,\n) -> Result<PumpEnd, String> {\n    loop {\n        // Controller: send the INITIAL terminal size so the PTY matches from the\n        // start (REQ-RCVIEW-1). Best-effort — a failure just leaves the default\n        // size. When the status row is active the harness PTY gets rows-1 (we own\n        // row 1), never 0 (REQ-RC-IDENTITY). Re-sent per establish: the new\n        // viewport's PTY must match our CURRENT terminal, not the original one.\n        if !view {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                let pty_rows = if status.is_some() {\n                    rows.saturating_sub(1).max(1)\n                } else {\n                    rows\n                };\n                let _ = send_attach_resize(&mut est.brain, est.stream_id, pty_rows, cols);\n            }\n        }\n\n        let mut decoder = AttachDecoder::new();\n        let end = pump(\n            &mut est.brain,\n            est.stream_id,\n            &est.ops,\n            rx,\n            &mut decoder,\n            out,\n            !view, // controller drives resize\n            mouse_mode,\n            status.as_deref_mut(),\n        );\n\n        // Final ends (incl. hard errors) exit the loop to the parting copy.\n        let severed_kind = match &end {\n            Ok(e) if reconnect_class(e) == EndClass::Reconnect => match e {\n                PumpEnd::BrokerGone => \"the daemon connection dropped\",\n                _ => \"the connection was severed\",\n            },\n            _ => return end,\n        };\n\n        // Reconnect: full-screen centered banner (operator language — the internal\n        // detail stays off the terminal per the Q4 UX rule), then re-drive\n        // establish_attach inside the bounded window. A detach keypress mid-window\n        // aborts the retry loop honestly. The banner repaints each iteration with a\n        // live COUNTDOWN (REQ-RC-RECONNECT-TRUTH) so the operator sees rc is bounded\n        // and working, never the old apparent freeze at a static \"Reconnecting…\".\n        // establish_attach is CONNECT-ONLY: a down daemon yields DaemonDown here and\n        // is retried (the operator may bring it back) WITHOUT any WMI resurrection.\n        let target = remote_node.clone().unwrap_or_else(|| \"local daemon\".to_string());\n        let retry_started = Instant::now();\n        let reestablished = loop {\n            let elapsed = retry_started.elapsed();\n            if reconnect_expired(elapsed) {\n                break None;\n            }\n            let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));\n            let remaining = reconnect_remaining_secs(elapsed);\n            let _ = out.write_all(&reconnect_banner_bytes(rows, cols, &target, remaining));\n            let _ = out.flush();\n            // Operator gives up mid-reconnect: honor the detach keys.\n            if let Some(rx) = rx {\n                let mut detached = false;\n                while let Ok(msg) = rx.try_recv() {\n                    if matches!(msg, StdinMsg::Detach) {\n                        detached = true;\n                    }\n                }\n                if detached {\n                    break Some(Err(())); // detach sentinel\n                }\n            }\n            match establish_attach(endpoint_id, intent) {\n                Ok(next) => break Some(Ok(next)),\n                // Every fail shape retries until the window expires — the node/daemon\n                // may still be coming back. DaemonDown included: rc waits for the\n                // operator to restart it, it does NOT auto-start one.\n                Err(_) => std::thread::sleep(RECONNECT_PAUSE),\n            }\n        };\n        match reestablished {\n            Some(Ok(next)) => {\n                *remote_node = next.remote_node.clone();\n                est = next;\n                // Loop: fresh resize + pump. The re-serve replays the output ring,\n                // repainting the screen the banner cleared.\n            }\n            Some(Err(())) => return Ok(PumpEnd::Detached),\n            None => {\n                // Give-up copy selection (REQ-RC-RECONNECT-TRUTH): a still-down local\n                // daemon (only knowable for a LOCAL target) gets the loud \"session\n                // lost — daemon down\" copy; anything else keeps the generic\n                // didn't-reconnect copy. Remote severs never probe local daemon state.\n                let daemon_down =\n                    remote_node.is_none() && !spt_daemon::daemon::is_running();\n                return Ok(PumpEnd::ReconnectGaveUp {\n                    detail: severed_kind.to_string(),\n                    daemon_down,\n                });\n            }\n        }\n    }\n}\n\n/// How long the B-3 reconnect loop keeps re-driving `establish_attach` after a\n/// severed viewport before the plain-language give-up (REQ-RC-RECONNECT).\n/// Generous: a daemon bounce (`spt daemon stop` + the next auto-start) and a\n/// node's broker restart both land well inside it.\nconst RECONNECT_WINDOW: Duration = Duration::from_secs(30);\n/// The pause between reconnect attempts (each attempt already carries its own\n/// bounded setup deadlines — this just keeps the loop from hammering).\nconst RECONNECT_PAUSE: Duration = Duration::from_secs(1);\n\n/// The give-up decision, pulled out so it unit-tests without a 30s wait.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_expired(elapsed: Duration) -> bool {\n    elapsed >= RECONNECT_WINDOW\n}\n\n/// Seconds still left in the reconnect window, for the banner countdown\n/// (REQ-RC-RECONNECT-TRUTH). CEIL so the display reads a full `RECONNECT_WINDOW`s\n/// at the start and never flashes `0s` while the loop is still trying (0 only once\n/// the window is fully spent, where the caller has already broken to give-up).\n// [impl->REQ-RC-RECONNECT-TRUTH]\nfn reconnect_remaining_secs(elapsed: Duration) -> u64 {\n    let left = RECONNECT_WINDOW.saturating_sub(elapsed);\n    // ceil(left / 1s) via integer millis — 1..=WINDOW while any time remains, 0 at/past.\n    (left.as_millis() as u64).div_ceil(1000)\n}\n\n/// Which pump ends re-drive the viewport (B-3) vs. exit with parting copy.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum EndClass {\n    Final,\n    Reconnect,\n}\n\n/// A severed transport is RECONNECTABLE: the broker-conn EOF class (broker\n/// bounce) and the serve-side EOF after rendered output (remote conn drop).\n/// Everything else — session exit, operator detach, displacement, the stall\n/// backstop, an attach refuse — is FINAL: re-driving those would re-attach to a\n/// session the operator (or the session itself) deliberately ended.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_class(end: &PumpEnd) -> EndClass {\n    match end {\n        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n        PumpEnd::Exited(_)\n        | PumpEnd::Detached\n        | PumpEnd::Displaced(_)\n        | PumpEnd::Stalled\n        | PumpEnd::NoLiveSession\n        | PumpEnd::ReconnectGaveUp { .. } => EndClass::Final,\n    }\n}\n\n/// Classify a serve-side stream EOF: rendered output first ⇒ the conn was\n/// SEVERED under a live viewport (reconnect); nothing ever rendered ⇒ the serve\n/// REFUSED the attach (no live session / gate deny — the cross-node D6\n/// stale-row case), honest \"no live session\" instead of a false \"detached\"\n/// (#4, REQ-RC-CROSS-NODE-ATTACH).\n// [impl->REQ-RC-RECONNECT]\nfn serve_eof_end(rendered_any: bool) -> PumpEnd {\n    if rendered_any {\n        PumpEnd::Severed\n    } else {\n        PumpEnd::NoLiveSession\n    }\n}\n\n/// The full-screen reconnect banner (B-3): clear + home, then the operator-language\n/// line centered on the terminal. Pure byte-emit (like [`StatusRow`]) so the exact\n/// VT shape unit-tests directly; the caller writes the bytes. The text is plain\n/// operator language (Q4 UX rule) — the internal sever detail never paints here.\n// [impl->REQ-RC-RECONNECT]\nfn reconnect_banner_bytes(rows: u16, cols: u16, target: &str, remaining_secs: u64) -> Vec<u8> {\n    // Visible countdown (REQ-RC-RECONNECT-TRUTH): the remaining-seconds suffix turns\n    // the old static banner into a live \"rc is working, bounded\" signal — the operator\n    // never mistakes a bounded retry for the historical freeze-forever.\n    let text = format!(\"Reconnecting to {target}… {remaining_secs}s\");\n    let row = (rows / 2).max(1);\n…\n    let mut out = Vec::new();\n    out.extend_from_slice(b\"\\x1b[2J\\x1b[H\");\n    out.extend_from_slice(format!(\"\\x1b[{row};{col}H\").as_bytes());\n    out.extend_from_slice(text.as_bytes());\n    out\n}\n\n#[derive(Debug)]\nenum PumpEnd {\n    Exited(Option<i32>),\n    Detached,\n    /// A different operator took control (REQ-KICK-1); carries the taker's node.\n    Displaced(String),\n    /// The broker stream was severed mid-session (broker bounce / hang-up — e.g. a\n    /// `spt daemon stop`). Recoverable, not a crash: the broker is the\n    /// daemon-lifetime anchor and returns on the next `spt api` call, so this\n    /// enters the B-3 reconnect loop (REQ-RC-RECONNECT) rather than exiting.\n    BrokerGone,\n    /// The serve side finished the stream AFTER rendering output — the remote\n    /// conn dropped (node bounce, network cut), NOT an operator detach (that\n    /// returns via the stdin `Detach` path directly) and NOT a session end (an\n    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\n    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\n    /// re-drives. [impl->REQ-RC-RECONNECT]\n    Severed,\n    /// B-3 terminal state: the reconnect window expired without a successful\n    /// re-attach. `detail` is the operator-language cause of the ORIGINAL sever\n    /// (Q4 UX rule — never read-error lingo). `daemon_down` records whether the\n    /// local daemon was still down at give-up (REQ-RC-RECONNECT-TRUTH) — it\n    /// selects the loud \"session lost — daemon down\" copy over the generic\n    /// didn't-reconnect copy. [impl->REQ-RC-RECONNECT] [impl->REQ-RC-RECONNECT-TRUTH]\n    ReconnectGaveUp { detail: String, daemon_down: bool },\n    /// The attach produced NO event whatsoever within the generous first-event\n    /// backstop window (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a healthy session —\n    /// even one still painting / mid-init — replays its buffered output (or at\n    /// least a Subscribed/Size frame) promptly, so a totally silent stream past\n    /// this bound is a dead/wedged session, not a slow one. Surfaces a clear\n    /// message instead of the historical infinite blank. The bound is deliberately\n    /// LONG (only ever bites a pathological hang; the broker-EOF-on-dead-child path\n    /// and the status-gate catch the ordinary dead cases first).\n    Stalled,\n    /// The serve side finished the stream having produced NO output — i.e. it\n    /// refused the attach (no live session for the endpoint, or the access gate\n    /// denied us). On the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH) this is\n    /// the D6 stale-row case: the registry routed us to a node that no longer has\n    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\n    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\n    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    NoLiveSession,\n}\n\n/// Generous first-event backstop for the attach pump (REQ-HAZARD-RC-ATTACH-FAILFAST\n/// path b): if NO broker event arrives within this window after attach, the\n/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\n/// session (empty OutputLog, still painting) MUST pass, so this only catches a\n/// true hang, never the working fresh-attach.\nconst FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\n\n/// The path-b stall decision (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach has\n/// stalled iff NO broker event has ever arrived AND the generous grace has\n/// elapsed. Pulled out so the decision is unit-testable without a 30s pump run.\n// [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\nfn first_event_stalled(seen_any: bool, elapsed: Duration) -> bool {\n    !seen_any && elapsed >= FIRST_EVENT_GRACE\n}\n\n/// How the rc pump treats a `read_event_until` error (REQ-HAZARD-RC-EOF).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ReadDisposition {\n    /// Transient poll-slice timeout — loop and poll again.\n    Retry,\n    /// Severed broker stream (broker bounced / hung up) — recoverable, surface\n    /// gracefully.\n    BrokerGone,\n    /// A genuine read failure — propagate.\n    Fatal,\n}\n\n/// Classify a `read_event_until` error kind. A poll-slice timeout retries; a\n/// severed-broker-stream EOF (the broker bounced — e.g. `spt daemon stop` —\n/// surfacing as `UnexpectedEof` \"failed to fill whole buffer\", or a\n/// reset/abort/broken-pipe in the same class) is RECOVERABLE and surfaces\n/// gracefully rather than crashing the PTY with the raw io text (the same EOF\n/// family the v0.9.1 seed fix handles); anything else is fatal.\n// [impl->REQ-HAZARD-RC-EOF]\nfn classify_read_err(kind: std::io::ErrorKind) -> ReadDisposition {\n    use std::io::ErrorKind::*;\n    match kind {\n        WouldBlock | TimedOut => ReadDisposition::Retry,\n        UnexpectedEof | BrokenPipe | ConnectionReset | ConnectionAborted => {\n            ReadDisposition::BrokerGone\n        }\n        _ => ReadDisposition::Fatal,\n    }\n}\n\n/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n#[allow(clippy::too_many_arguments)]\nfn pump(\n    brain: &mut Brain,\n    stream_id: u64,\n    ops: &OpMinter,\n    rx: Option<&mpsc::Receiver<StdinMsg>>,\n    decoder: &mut AttachDecoder,\n    stdout: &mut impl std::io::Write,\n    controller: bool,\n    mouse_mode: &MouseMode,\n    mut status: Option<&mut StatusRow>,\n) -> Result<PumpEnd, String> {\n    // Track the harness's mouse-reporting mode from its output DECSET sequences so\n    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The\n    // scanner is pump-local (single producer) and survives a sequence split across\n    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.\n    let mut mouse_scanner = MouseModeScanner::default();\n    // The reserved-row re-assert scanner (REQ-RC-IDENTITY): watches the harness\n    // output for an alt-screen enter / DECSTBM reset that would destroy our\n    // scroll-region margin, so we can re-paint the status row. Pump-local, single\n    // producer, survives a trigger split across output chunks (like mouse_scanner).\n    let mut reassert_scanner = ReassertScanner::default();\n    // Reserve the top row + paint the identity banner up front (REQ-RC-IDENTITY).\n    // Controller-only (the caller passes `Some` only for a control attach).\n    if let Some(status) = status.as_deref_mut() {\n        stdout\n            .write_all(&status.assert_bytes())\n            .map_err(|e| format!(\"status assert: {e}\"))?;\n        stdout.flush().map_err(|e| format!(\"status flush: {e}\"))?;\n    }\n    // Finish our send side so the target's `serve_attach` sees EOF → Detached\n    // (the session outlives the viewport). Best-effort: an already-finished or\n    // exited stream send is harmless to attempt.\n    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {\n        let _ = brain.net_stream_send(stream_id, &[], None, true);\n        PumpEnd::Detached\n    }\n    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n    // drop any `seq` already rendered (the operator render cursor, matching the\n    // local `OutputLog` replay discipline).\n    let mut cursor = 0u64;\n    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n    // each slice and forward a Resize when it changes (cross-platform without\n    // hooking SIGWINCH / the crossterm event loop, which would conflict with the\n    // raw-stdin reader). `None` until the first read.\n    let mut last_size: Option<(u16, u16)> = None;\n    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown\n    // as a one-line indicator when it differs from our own terminal.\n    let mut pty_size: Option<(u16, u16)> = None;\n    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged\n    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE\n    // rather than an infinite blank. Any broker event clears it.\n    let started = Instant::now();\n    let mut seen_any = false;\n    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n    // Any live session replays its buffered ring on attach, so a real attach sets\n    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n    let mut rendered_any = false;\n    loop {\n        // ── controller window-change → resize ──────────────────────────────\n        if controller {\n            if let Ok((cols, rows)) = crossterm::terminal::size() {\n                if last_size != Some((rows, cols)) {\n                    last_size = Some((rows, cols));\n                    match status.as_deref_mut() {\n                        // Status active: the harness PTY owns rows-1 (we keep row 1\n                        // for the banner), and we re-assert the margin + repaint on\n                        // the new size (REQ-RC-IDENTITY). Never resize the PTY to 0.\n                        Some(status) => {\n                            status.update_size(rows, cols);\n                            let pty_rows = rows.saturating_sub(1).max(1);\n                            let _ = send_attach_resize(brain, stream_id, pty_rows, cols);\n                            if stdout.write_all(&status.assert_bytes()).is_ok() {\n                                let _ = stdout.flush();\n                            }\n                        }\n                        None => {\n                            let _ = send_attach_resize(brain, stream_id, rows, cols);\n                        }\n                    }\n                }\n            }\n        }\n\n        // ── stdin → session ────────────────────────────────────────────────\n        if let Some(rx) = rx {\n            loop {\n                match rx.try_recv() {\n                    Ok(StdinMsg::Bytes(bytes)) => {\n                        send_attach_input(brain, stream_id, &bytes, ops.next())\n                            .map_err(|e| format!(\"send input: {e}\"))?;\n                    }\n                    Ok(StdinMsg::Detach) => return Ok(detach(brain, stream_id)),\n                    Err(mpsc::TryRecvError::Empty) => break,\n                    Err(mpsc::TryRecvError::Disconnected) => {\n                        return Ok(detach(brain, stream_id))\n                    }\n                }\n            }\n        }\n\n        // ── session → terminal ─────────────────────────────────────────────\n        let ev = match brain.read_event_until(Some(Instant::now() + POLL_SLICE)) {\n            Ok(ev) => ev,\n            Err(e) => match classify_read_err(e.kind()) {\n                // A poll-slice timeout: no event this slice. Before looping, the\n                // first-event backstop — if NOTHING has ever arrived past the\n                // generous grace, the attach is dead/wedged (path b).\n                ReadDisposition::Retry => {\n                    if first_event_stalled(seen_any, started.elapsed()) {\n                        return Ok(PumpEnd::Stalled);\n                    }\n                    continue;\n                }\n                // A severed broker stream (broker bounce / hang-up) — surface it\n                // gracefully, never as a raw RC_FAIL crash (REQ-HAZARD-RC-EOF).\n                ReadDisposition::BrokerGone => return Ok(PumpEnd::BrokerGone),\n                ReadDisposition::Fatal => return Err(format!(\"read event: {e}\")),\n            },\n        };\n        seen_any = true; // any broker event proves the attach stream is live\n        match ev {\n            BrokerEvent::NetStreamData { stream_id: sid, bytes, .. } if sid == stream_id => {\n                for rec in decoder.push(&bytes) {\n                    match rec {\n                        AttachRecord::Output { seq, data_b64 } => {\n                            if seq < cursor {\n                                continue; // already rendered — re-serve dedup\n                            }\n                            cursor = seq + 1;\n                            rendered_any = true;\n                            let bytes = decode_bytes(&data_b64)\n                                .map_err(|e| format!(\"decode output: {e}\"))?;\n                            // Track the harness's mouse-reporting mode from its output\n                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n                            mouse_scanner.feed(mouse_mode, &bytes);\n                            stdout.write_all(&bytes).map_err(|e| format!(\"write: {e}\"))?;\n                            // Re-assert the reserved row if the harness output just\n                            // destroyed our scroll region — alt-screen enter or a\n                            // DECSTBM reset (REQ-RC-IDENTITY). The trigger MEANS the\n                            // margin is gone (alt-screen has its own full-screen\n                            // margins; a DECSTBM reset clears ours), so re-set the\n                            // MARGIN + repaint (assert_bytes), not just the text —\n                            // else the harness scrolls back over row 1. Scanned on the\n                            // SAME bytes we rendered, after writing them.\n                            if let Some(status) = status.as_deref_mut() {\n                                if reassert_scanner.feed(&bytes) {\n                                    stdout\n                                        .write_all(&status.assert_bytes())\n                                        .map_err(|e| format!(\"status reassert: {e}\"))?;\n                                }\n                            }\n                            stdout.flush().map_err(|e| format!(\"flush: {e}\"))?;\n                        }\n                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n                        AttachRecord::Displaced { by } => {\n                            return Ok(PumpEnd::Displaced(by));\n                        }\n                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,\n                        // show a one-line indicator when our terminal differs from\n                        // the new PTY size (rc is a byte pump — true clip/pad needs\n                        // a grid model, deferred; the honest size cue renders here).\n                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {\n                            pty_size = Some((rows, cols));\n                            match crossterm::terminal::size() {\n                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {\n                                    let _ = write!(\n                                        stdout,\n                                        \"\\r\\n[viewing {cols}x{rows} — your terminal is {mycols}x{myrows}]\\r\\n\"\n                                    );\n                                    let _ = stdout.flush();\n                                }\n                                _ => {}\n                            }\n                        }\n                        _ => {} // Request/Input echoed back: protocol noise, ignore.\n                    }\n                }\n            }\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\n                return Ok(serve_eof_end(rendered_any));\n            }\n            BrokerEvent::Error { message } => return Err(message),\n            _ => {} // acks + unrelated events: not ours.\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n…\n}","startLine":1379,"lineNumbers":[1379,null,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,null,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,null,3241]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"}}},"isError":false,"timestamp":1784365505153}}
{"type":"message","id":"4571ae56","parentId":"4b4071d4","timestamp":"2026-07-18T09:05:05.168Z","message":{"role":"toolResult","toolCallId":"call_siFsUWmxBfFXHp4lNkV0i0l8|fc_0ce5441d29b9f05d016a5b41c0fbf4819b81b818855d5ca6eb","toolName":"read","content":[{"type":"text","text":"    fn buf(&self) -> &Vec<Vec<Cell>> {\n        if self.alt_active {\n            &self.alt\n        } else {\n            &self.main\n        }\n    }\n\n    fn buf_mut(&mut self) -> &mut Vec<Vec<Cell>> {\n        if self.alt_active {\n            &mut self.alt\n        } else {\n            &mut self.main\n        }\n    }\n\n    /// The pen an erase fills with: attributes cleared but the current background\n    /// preserved (so a status-bar erase keeps its colour).\n    fn erase_pen(&self) -> Pen {\n        Pen {\n            bg: self.pen.bg,\n            ..Pen::default()\n        }\n    }\n\n    fn blank(&self) -> Cell {\n        Cell {\n            ch: ' ',\n            pen: self.erase_pen(),\n        }\n    }\n\n    fn clamp_cursor(&mut self) {\n        if self.row >= self.rows {\n            self.row = self.rows - 1;\n        }\n        if self.col >= self.cols {\n            self.col = self.cols - 1;\n        }\n    }\n\n    /// Line feed within the scroll region: advance a row, scrolling the region up\n    /// when at its bottom margin.\n    fn line_feed(&mut self) {\n        if self.row == self.scroll_bottom {\n            self.scroll_up(1);\n        } else if self.row + 1 < self.rows {\n            self.row += 1;\n        }\n    }\n\n    /// Reverse line feed: retreat a row, scrolling the region down at the top margin.\n    fn reverse_line_feed(&mut self) {\n        if self.row == self.scroll_top {\n            self.scroll_down(1);\n        } else if self.row > 0 {\n            self.row -= 1;\n        }\n    }\n\n    fn scroll_up(&mut self, n: usize) {\n        let blank = self.blank();\n        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n        let cols = self.cols;\n        let buf = self.buf_mut();\n        for _ in 0..n {\n            if top <= bottom {\n                buf[top..=bottom].rotate_left(1);\n                buf[bottom] = vec![blank; cols];\n            }\n        }\n    }\n\n    fn scroll_down(&mut self, n: usize) {\n        let blank = self.blank();\n        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n        let cols = self.cols;\n        let buf = self.buf_mut();\n        for _ in 0..n {\n            if top <= bottom {\n                buf[top..=bottom].rotate_right(1);\n                buf[top] = vec![blank; cols];\n            }\n        }\n    }\n\n    /// Print a character at the cursor, honouring deferred auto-wrap (the char in\n    /// the last column does not advance until the next print).\n    fn put_char(&mut self, c: char) {\n        if self.pending_wrap {\n            self.col = 0;\n            self.line_feed();\n            self.pending_wrap = false;\n        }\n        let (row, col) = (self.row, self.col);\n        let pen = self.pen;\n        if row < self.rows && col < self.cols {\n            self.buf_mut()[row][col] = Cell { ch: c, pen };\n        }\n        if self.col + 1 >= self.cols {\n            self.pending_wrap = true;\n        } else {\n            self.col += 1;\n        }\n    }\n\n    // --- CSI handlers -------------------------------------------------------\n\n    fn erase_display(&mut self, mode: u16) {\n        let blank = self.blank();\n        let (row, col, rows, cols) = (self.row, self.col, self.rows, self.cols);\n        let buf = self.buf_mut();\n        match mode {\n            0 => {\n                // cursor → end of screen\n                for cell in &mut buf[row][col..cols] {\n                    *cell = blank;\n                }\n                for line in &mut buf[(row + 1)..rows] {\n                    for cell in line.iter_mut() {\n                        *cell = blank;\n                    }\n                }\n            }\n            1 => {\n                // start of screen → cursor\n                for line in &mut buf[0..row] {\n                    for cell in line.iter_mut() {\n                        *cell = blank;\n                    }\n                }\n                for cell in &mut buf[row][0..=col.min(cols - 1)] {\n                    *cell = blank;\n                }\n            }\n            // 2 (all) and 3 (all + scrollback — we hold no scrollback) both clear all.\n            _ => {\n                for line in buf.iter_mut() {\n                    for cell in line.iter_mut() {\n                        *cell = blank;\n                    }\n                }\n            }\n        }\n    }\n\n    fn erase_line(&mut self, mode: u16) {\n        let blank = self.blank();\n        let (row, col, cols) = (self.row, self.col, self.cols);\n        let buf = self.buf_mut();\n        match mode {\n            0 => {\n                for cell in &mut buf[row][col..cols] {\n                    *cell = blank;\n                }\n            }\n            1 => {\n                for cell in &mut buf[row][0..=col.min(cols - 1)] {\n                    *cell = blank;\n                }\n            }\n            _ => {\n                for cell in buf[row].iter_mut() {\n                    *cell = blank;\n                }\n            }\n        }\n    }\n\n    fn erase_chars(&mut self, n: usize) {\n        let blank = self.blank();\n        let (row, col, cols) = (self.row, self.col, self.cols);\n        let buf = self.buf_mut();\n        for cell in &mut buf[row][col..(col + n).min(cols)] {\n            *cell = blank;\n        }\n    }\n\n    fn insert_chars(&mut self, n: usize) {\n        let blank = self.blank();\n        let (row, col, cols) = (self.row, self.col, self.cols);\n        let buf = self.buf_mut();\n        let n = n.min(cols - col);\n        for c in (col..cols).rev() {\n            buf[row][c] = if c >= col + n {\n                buf[row][c - n]\n            } else {\n                blank\n            };\n        }\n    }\n\n    fn delete_chars(&mut self, n: usize) {\n        let blank = self.blank();\n        let (row, col, cols) = (self.row, self.col, self.cols);\n        let buf = self.buf_mut();\n        let n = n.min(cols - col);\n        for c in col..cols {\n            buf[row][c] = if c + n < cols { buf[row][c + n] } else { blank };\n        }\n    }\n\n    fn insert_lines(&mut self, n: usize) {\n        if self.row < self.scroll_top || self.row > self.scroll_bottom {\n            return;\n        }\n        let blank = self.blank();\n        let (row, bottom, cols) = (self.row, self.scroll_bottom, self.cols);\n        let buf = self.buf_mut();\n        let n = n.min(bottom - row + 1);\n        for _ in 0..n {\n            buf[row..=bottom].rotate_right(1);\n            buf[row] = vec![blank; cols];\n        }\n    }\n\n    fn delete_lines(&mut self, n: usize) {\n        if self.row < self.scroll_top || self.row > self.scroll_bottom {\n            return;\n        }\n        let blank = self.blank();\n        let (row, bottom, cols) = (self.row, self.scroll_bottom, self.cols);\n        let buf = self.buf_mut();\n        let n = n.min(bottom - row + 1);\n        for _ in 0..n {\n            buf[row..=bottom].rotate_left(1);\n            buf[bottom] = vec![blank; cols];\n        }\n    }\n\n    fn set_mode(&mut self, private: bool, param: u16, enable: bool) {\n        if !private {\n            return;\n        }\n        match param {\n            25 => self.cursor_visible = enable, // DECTCEM\n            47 | 1047 => self.switch_alt(enable, false),\n            1049 => self.switch_alt(enable, true),\n            _ => {}\n        }\n    }\n\n    /// Enter/leave the alt screen. `save_restore` (mode 1049) also saves the cursor\n    /// on enter / restores it on leave, and clears the alt buffer on entry.\n    fn switch_alt(&mut self, enter: bool, save_restore: bool) {\n        if enter == self.alt_active {\n            if enter && save_restore {\n                // Re-entering alt still clears it (idempotent enter is rare but safe).\n                self.alt = blank_buffer(self.rows, self.cols);\n            }\n            return;\n        }\n        if enter {\n            if save_restore {\n                self.saved = Some((self.row, self.col));\n            }\n            self.alt = blank_buffer(self.rows, self.cols);\n            self.alt_active = true;\n            self.row = 0;\n            self.col = 0;\n            self.pending_wrap = false;\n        } else {\n            self.alt_active = false;\n            if save_restore {\n                if let Some((r, c)) = self.saved.take() {\n…\n                }\n            }\n…\n        }\n    }\n\n[Showing lines 239-503 of 1023. Use :504 to continue]"}],"details":{"truncation":{"content":"    fn buf(&self) -> &Vec<Vec<Cell>> {\r\n        if self.alt_active {\r\n            &self.alt\r\n        } else {\r\n            &self.main\r\n        }\r\n    }\r\n\r\n    fn buf_mut(&mut self) -> &mut Vec<Vec<Cell>> {\r\n        if self.alt_active {\r\n            &mut self.alt\r\n        } else {\r\n            &mut self.main\r\n        }\r\n    }\r\n\r\n    /// The pen an erase fills with: attributes cleared but the current background\r\n    /// preserved (so a status-bar erase keeps its colour).\r\n    fn erase_pen(&self) -> Pen {\r\n        Pen {\r\n            bg: self.pen.bg,\r\n            ..Pen::default()\r\n        }\r\n    }\r\n\r\n    fn blank(&self) -> Cell {\r\n        Cell {\r\n            ch: ' ',\r\n            pen: self.erase_pen(),\r\n        }\r\n    }\r\n\r\n    fn clamp_cursor(&mut self) {\r\n        if self.row >= self.rows {\r\n            self.row = self.rows - 1;\r\n        }\r\n        if self.col >= self.cols {\r\n            self.col = self.cols - 1;\r\n        }\r\n    }\r\n\r\n    /// Line feed within the scroll region: advance a row, scrolling the region up\r\n    /// when at its bottom margin.\r\n    fn line_feed(&mut self) {\r\n        if self.row == self.scroll_bottom {\r\n            self.scroll_up(1);\r\n        } else if self.row + 1 < self.rows {\r\n            self.row += 1;\r\n        }\r\n    }\r\n\r\n    /// Reverse line feed: retreat a row, scrolling the region down at the top margin.\r\n    fn reverse_line_feed(&mut self) {\r\n        if self.row == self.scroll_top {\r\n            self.scroll_down(1);\r\n        } else if self.row > 0 {\r\n            self.row -= 1;\r\n        }\r\n    }\r\n\r\n    fn scroll_up(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\r\n        let cols = self.cols;\r\n        let buf = self.buf_mut();\r\n        for _ in 0..n {\r\n            if top <= bottom {\r\n                buf[top..=bottom].rotate_left(1);\r\n                buf[bottom] = vec![blank; cols];\r\n            }\r\n        }\r\n    }\r\n\r\n    fn scroll_down(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\r\n        let cols = self.cols;\r\n        let buf = self.buf_mut();\r\n        for _ in 0..n {\r\n            if top <= bottom {\r\n                buf[top..=bottom].rotate_right(1);\r\n                buf[top] = vec![blank; cols];\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Print a character at the cursor, honouring deferred auto-wrap (the char in\r\n    /// the last column does not advance until the next print).\r\n    fn put_char(&mut self, c: char) {\r\n        if self.pending_wrap {\r\n            self.col = 0;\r\n            self.line_feed();\r\n            self.pending_wrap = false;\r\n        }\r\n        let (row, col) = (self.row, self.col);\r\n        let pen = self.pen;\r\n        if row < self.rows && col < self.cols {\r\n            self.buf_mut()[row][col] = Cell { ch: c, pen };\r\n        }\r\n        if self.col + 1 >= self.cols {\r\n            self.pending_wrap = true;\r\n        } else {\r\n            self.col += 1;\r\n        }\r\n    }\r\n\r\n    // --- CSI handlers -------------------------------------------------------\r\n\r\n    fn erase_display(&mut self, mode: u16) {\r\n        let blank = self.blank();\r\n        let (row, col, rows, cols) = (self.row, self.col, self.rows, self.cols);\r\n        let buf = self.buf_mut();\r\n        match mode {\r\n            0 => {\r\n                // cursor → end of screen\r\n                for cell in &mut buf[row][col..cols] {\r\n                    *cell = blank;\r\n                }\r\n                for line in &mut buf[(row + 1)..rows] {\r\n                    for cell in line.iter_mut() {\r\n                        *cell = blank;\r\n                    }\r\n                }\r\n            }\r\n            1 => {\r\n                // start of screen → cursor\r\n                for line in &mut buf[0..row] {\r\n                    for cell in line.iter_mut() {\r\n                        *cell = blank;\r\n                    }\r\n                }\r\n                for cell in &mut buf[row][0..=col.min(cols - 1)] {\r\n                    *cell = blank;\r\n                }\r\n            }\r\n            // 2 (all) and 3 (all + scrollback — we hold no scrollback) both clear all.\r\n            _ => {\r\n                for line in buf.iter_mut() {\r\n                    for cell in line.iter_mut() {\r\n                        *cell = blank;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    fn erase_line(&mut self, mode: u16) {\r\n        let blank = self.blank();\r\n        let (row, col, cols) = (self.row, self.col, self.cols);\r\n        let buf = self.buf_mut();\r\n        match mode {\r\n            0 => {\r\n                for cell in &mut buf[row][col..cols] {\r\n                    *cell = blank;\r\n                }\r\n            }\r\n            1 => {\r\n                for cell in &mut buf[row][0..=col.min(cols - 1)] {\r\n                    *cell = blank;\r\n                }\r\n            }\r\n            _ => {\r\n                for cell in buf[row].iter_mut() {\r\n                    *cell = blank;\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    fn erase_chars(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (row, col, cols) = (self.row, self.col, self.cols);\r\n        let buf = self.buf_mut();\r\n        for cell in &mut buf[row][col..(col + n).min(cols)] {\r\n            *cell = blank;\r\n        }\r\n    }\r\n\r\n    fn insert_chars(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (row, col, cols) = (self.row, self.col, self.cols);\r\n        let buf = self.buf_mut();\r\n        let n = n.min(cols - col);\r\n        for c in (col..cols).rev() {\r\n            buf[row][c] = if c >= col + n {\r\n                buf[row][c - n]\r\n            } else {\r\n                blank\r\n            };\r\n        }\r\n    }\r\n\r\n    fn delete_chars(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (row, col, cols) = (self.row, self.col, self.cols);\r\n        let buf = self.buf_mut();\r\n        let n = n.min(cols - col);\r\n        for c in col..cols {\r\n            buf[row][c] = if c + n < cols { buf[row][c + n] } else { blank };\r\n        }\r\n    }\r\n\r\n    fn insert_lines(&mut self, n: usize) {\r\n        if self.row < self.scroll_top || self.row > self.scroll_bottom {\r\n            return;\r\n        }\r\n        let blank = self.blank();\r\n        let (row, bottom, cols) = (self.row, self.scroll_bottom, self.cols);\r\n        let buf = self.buf_mut();\r\n        let n = n.min(bottom - row + 1);\r\n        for _ in 0..n {\r\n            buf[row..=bottom].rotate_right(1);\r\n            buf[row] = vec![blank; cols];\r\n        }\r\n    }\r\n\r\n    fn delete_lines(&mut self, n: usize) {\r\n        if self.row < self.scroll_top || self.row > self.scroll_bottom {\r\n            return;\r\n        }\r\n        let blank = self.blank();\r\n        let (row, bottom, cols) = (self.row, self.scroll_bottom, self.cols);\r\n        let buf = self.buf_mut();\r\n        let n = n.min(bottom - row + 1);\r\n        for _ in 0..n {\r\n            buf[row..=bottom].rotate_left(1);\r\n            buf[bottom] = vec![blank; cols];\r\n        }\r\n    }\r\n\r\n    fn set_mode(&mut self, private: bool, param: u16, enable: bool) {\r\n        if !private {\r\n            return;\r\n        }\r\n        match param {\r\n            25 => self.cursor_visible = enable, // DECTCEM\r\n            47 | 1047 => self.switch_alt(enable, false),\r\n            1049 => self.switch_alt(enable, true),\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    /// Enter/leave the alt screen. `save_restore` (mode 1049) also saves the cursor\r\n    /// on enter / restores it on leave, and clears the alt buffer on entry.\r\n    fn switch_alt(&mut self, enter: bool, save_restore: bool) {\r\n        if enter == self.alt_active {\r\n            if enter && save_restore {\r\n                // Re-entering alt still clears it (idempotent enter is rare but safe).\r\n                self.alt = blank_buffer(self.rows, self.cols);\r\n            }\r\n            return;\r\n        }\r\n        if enter {\r\n            if save_restore {\r\n                self.saved = Some((self.row, self.col));\r\n            }\r\n            self.alt = blank_buffer(self.rows, self.cols);\r\n            self.alt_active = true;\r\n            self.row = 0;\r\n            self.col = 0;\r\n            self.pending_wrap = false;\r\n        } else {\r\n            self.alt_active = false;\r\n            if save_restore {\r\n                if let Some((r, c)) = self.saved.take() {\r","truncated":true,"truncatedBy":"lines","totalLines":785,"totalBytes":8416,"outputLines":265,"outputBytes":8416,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    fn buf(&self) -> &Vec<Vec<Cell>> {\r\n        if self.alt_active {\r\n            &self.alt\r\n        } else {\r\n            &self.main\r\n        }\r\n    }\r\n\r\n    fn buf_mut(&mut self) -> &mut Vec<Vec<Cell>> {\r\n        if self.alt_active {\r\n            &mut self.alt\r\n        } else {\r\n            &mut self.main\r\n        }\r\n    }\r\n\r\n    /// The pen an erase fills with: attributes cleared but the current background\r\n    /// preserved (so a status-bar erase keeps its colour).\r\n    fn erase_pen(&self) -> Pen {\r\n        Pen {\r\n            bg: self.pen.bg,\r\n            ..Pen::default()\r\n        }\r\n    }\r\n\r\n    fn blank(&self) -> Cell {\r\n        Cell {\r\n            ch: ' ',\r\n            pen: self.erase_pen(),\r\n        }\r\n    }\r\n\r\n    fn clamp_cursor(&mut self) {\r\n        if self.row >= self.rows {\r\n            self.row = self.rows - 1;\r\n        }\r\n        if self.col >= self.cols {\r\n            self.col = self.cols - 1;\r\n        }\r\n    }\r\n\r\n    /// Line feed within the scroll region: advance a row, scrolling the region up\r\n    /// when at its bottom margin.\r\n    fn line_feed(&mut self) {\r\n        if self.row == self.scroll_bottom {\r\n            self.scroll_up(1);\r\n        } else if self.row + 1 < self.rows {\r\n            self.row += 1;\r\n        }\r\n    }\r\n\r\n    /// Reverse line feed: retreat a row, scrolling the region down at the top margin.\r\n    fn reverse_line_feed(&mut self) {\r\n        if self.row == self.scroll_top {\r\n            self.scroll_down(1);\r\n        } else if self.row > 0 {\r\n            self.row -= 1;\r\n        }\r\n    }\r\n\r\n    fn scroll_up(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\r\n        let cols = self.cols;\r\n        let buf = self.buf_mut();\r\n        for _ in 0..n {\r\n            if top <= bottom {\r\n                buf[top..=bottom].rotate_left(1);\r\n                buf[bottom] = vec![blank; cols];\r\n            }\r\n        }\r\n    }\r\n\r\n    fn scroll_down(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\r\n        let cols = self.cols;\r\n        let buf = self.buf_mut();\r\n        for _ in 0..n {\r\n            if top <= bottom {\r\n                buf[top..=bottom].rotate_right(1);\r\n                buf[top] = vec![blank; cols];\r\n            }\r\n        }\r\n    }\r\n\r\n    /// Print a character at the cursor, honouring deferred auto-wrap (the char in\r\n    /// the last column does not advance until the next print).\r\n    fn put_char(&mut self, c: char) {\r\n        if self.pending_wrap {\r\n            self.col = 0;\r\n            self.line_feed();\r\n            self.pending_wrap = false;\r\n        }\r\n        let (row, col) = (self.row, self.col);\r\n        let pen = self.pen;\r\n        if row < self.rows && col < self.cols {\r\n            self.buf_mut()[row][col] = Cell { ch: c, pen };\r\n        }\r\n        if self.col + 1 >= self.cols {\r\n            self.pending_wrap = true;\r\n        } else {\r\n            self.col += 1;\r\n        }\r\n    }\r\n\r\n    // --- CSI handlers -------------------------------------------------------\r\n\r\n    fn erase_display(&mut self, mode: u16) {\r\n        let blank = self.blank();\r\n        let (row, col, rows, cols) = (self.row, self.col, self.rows, self.cols);\r\n        let buf = self.buf_mut();\r\n        match mode {\r\n            0 => {\r\n                // cursor → end of screen\r\n                for cell in &mut buf[row][col..cols] {\r\n                    *cell = blank;\r\n                }\r\n                for line in &mut buf[(row + 1)..rows] {\r\n                    for cell in line.iter_mut() {\r\n                        *cell = blank;\r\n                    }\r\n                }\r\n            }\r\n            1 => {\r\n                // start of screen → cursor\r\n                for line in &mut buf[0..row] {\r\n                    for cell in line.iter_mut() {\r\n                        *cell = blank;\r\n                    }\r\n                }\r\n                for cell in &mut buf[row][0..=col.min(cols - 1)] {\r\n                    *cell = blank;\r\n                }\r\n            }\r\n            // 2 (all) and 3 (all + scrollback — we hold no scrollback) both clear all.\r\n            _ => {\r\n                for line in buf.iter_mut() {\r\n                    for cell in line.iter_mut() {\r\n                        *cell = blank;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    fn erase_line(&mut self, mode: u16) {\r\n        let blank = self.blank();\r\n        let (row, col, cols) = (self.row, self.col, self.cols);\r\n        let buf = self.buf_mut();\r\n        match mode {\r\n            0 => {\r\n                for cell in &mut buf[row][col..cols] {\r\n                    *cell = blank;\r\n                }\r\n            }\r\n            1 => {\r\n                for cell in &mut buf[row][0..=col.min(cols - 1)] {\r\n                    *cell = blank;\r\n                }\r\n            }\r\n            _ => {\r\n                for cell in buf[row].iter_mut() {\r\n                    *cell = blank;\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    fn erase_chars(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (row, col, cols) = (self.row, self.col, self.cols);\r\n        let buf = self.buf_mut();\r\n        for cell in &mut buf[row][col..(col + n).min(cols)] {\r\n            *cell = blank;\r\n        }\r\n    }\r\n\r\n    fn insert_chars(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (row, col, cols) = (self.row, self.col, self.cols);\r\n        let buf = self.buf_mut();\r\n        let n = n.min(cols - col);\r\n        for c in (col..cols).rev() {\r\n            buf[row][c] = if c >= col + n {\r\n                buf[row][c - n]\r\n            } else {\r\n                blank\r\n            };\r\n        }\r\n    }\r\n\r\n    fn delete_chars(&mut self, n: usize) {\r\n        let blank = self.blank();\r\n        let (row, col, cols) = (self.row, self.col, self.cols);\r\n        let buf = self.buf_mut();\r\n        let n = n.min(cols - col);\r\n        for c in col..cols {\r\n            buf[row][c] = if c + n < cols { buf[row][c + n] } else { blank };\r\n        }\r\n    }\r\n\r\n    fn insert_lines(&mut self, n: usize) {\r\n        if self.row < self.scroll_top || self.row > self.scroll_bottom {\r\n            return;\r\n        }\r\n        let blank = self.blank();\r\n        let (row, bottom, cols) = (self.row, self.scroll_bottom, self.cols);\r\n        let buf = self.buf_mut();\r\n        let n = n.min(bottom - row + 1);\r\n        for _ in 0..n {\r\n            buf[row..=bottom].rotate_right(1);\r\n            buf[row] = vec![blank; cols];\r\n        }\r\n    }\r\n\r\n    fn delete_lines(&mut self, n: usize) {\r\n        if self.row < self.scroll_top || self.row > self.scroll_bottom {\r\n            return;\r\n        }\r\n        let blank = self.blank();\r\n        let (row, bottom, cols) = (self.row, self.scroll_bottom, self.cols);\r\n        let buf = self.buf_mut();\r\n        let n = n.min(bottom - row + 1);\r\n        for _ in 0..n {\r\n            buf[row..=bottom].rotate_left(1);\r\n            buf[bottom] = vec![blank; cols];\r\n        }\r\n    }\r\n\r\n    fn set_mode(&mut self, private: bool, param: u16, enable: bool) {\r\n        if !private {\r\n            return;\r\n        }\r\n        match param {\r\n            25 => self.cursor_visible = enable, // DECTCEM\r\n            47 | 1047 => self.switch_alt(enable, false),\r\n            1049 => self.switch_alt(enable, true),\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    /// Enter/leave the alt screen. `save_restore` (mode 1049) also saves the cursor\r\n    /// on enter / restores it on leave, and clears the alt buffer on entry.\r\n    fn switch_alt(&mut self, enter: bool, save_restore: bool) {\r\n        if enter == self.alt_active {\r\n            if enter && save_restore {\r\n                // Re-entering alt still clears it (idempotent enter is rare but safe).\r\n                self.alt = blank_buffer(self.rows, self.cols);\r\n            }\r\n            return;\r\n        }\r\n        if enter {\r\n            if save_restore {\r\n                self.saved = Some((self.row, self.col));\r\n            }\r\n            self.alt = blank_buffer(self.rows, self.cols);\r\n            self.alt_active = true;\r\n            self.row = 0;\r\n            self.col = 0;\r\n            self.pending_wrap = false;\r\n        } else {\r\n            self.alt_active = false;\r\n            if save_restore {\r\n                if let Some((r, c)) = self.saved.take() {\r\n…\n                }\n            }\n…\n        }\n    }","startLine":239,"lineNumbers":[239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,null,506,507,null,510,511]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\screen.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1023,"totalBytes":8416,"outputLines":265,"outputBytes":8416,"shownRange":{"start":239,"end":503},"nextOffset":504}}},"isError":false,"timestamp":1784365505168}}
{"type":"message","id":"340d2bb9","parentId":"4571ae56","timestamp":"2026-07-18T09:05:05.175Z","message":{"role":"toolResult","toolCallId":"call_R1ySxe7weSPvSMdp0XpAWauA|fc_0ce5441d29b9f05d016a5b41c0fc1c819bac7478c6d236de7c","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/src/\n### rc.rs\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...\n 1837|/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n*1838|/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n 1839|#[allow(clippy::too_many_arguments)]\n 1840|fn pump(\n 1841|    brain: &mut Brain,\n...\n 1965|                    match rec {\n*1966|                        AttachRecord::Output { seq, data_b64 } => {\n 1967|                            if seq < cursor {\n 1968|                                continue; // already rendered — re-serve dedup\n 1969|                            }\n...\n 1971|                            rendered_any = true;\n*1972|                            let bytes = decode_bytes(&data_b64)\n 1973|                                .map_err(|e| format!(\"decode output: {e}\"))?;\n 1974|                            // Track the harness's mouse-reporting mode from its output\n 1975|                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n\n## spt-daemon/src/\n### attach.rs\n 7|//!\n*8|//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n 9|//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n 10|//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n 11|//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n...\n 52|use crate::effect::{Minter, MintedOp};\n*53|use crate::msg::{decode_bytes, encode_bytes};\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...\n 413|                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\n*414|                        let line = ndjson::encode_line(&AttachRecord::Output {\n 415|                            seq: brain.controller_resume_floor(),\n*416|                            data_b64: encode_bytes(notice),\n 417|                        });\n 418|                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n 419|                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n...\n 468|                        }\n*469|                        AttachRecord::Input { data_b64, op_id }\n 470|                            if attached && intent != AttachIntent::Viewer =>\n 471|                        {\n*472|                            let bytes = decode_bytes(&data_b64)\n 473|                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 474|                            match role {\n 475|                                // Confirmed controller — exactly-once at the PTY\n...\n 600|            BrokerEvent::Output { seq, bytes, .. } if attached => {\n*601|                let line = ndjson::encode_line(&AttachRecord::Output {\n 602|                    seq,\n*603|                    data_b64: encode_bytes(&bytes),\n 604|                });\n 605|                // Unjournaled by design (see module docs): `seq` rides inside\n 606|                // the record, and the operator's render cursor is the dedup.\n...\n 631|/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\n*632|/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\n 633|/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\n 634|/// durable id for the exactly-once stream open (safe to block for its ack —\n 635|/// no attach traffic can be in flight before the stream exists); the Request\n...\n 721|    let line = ndjson::encode_line(&AttachRecord::Input {\n*722|        data_b64: encode_bytes(bytes),\n 723|        op_id: wire_op,\n 724|    });\n 725|    brain.net_stream_send(stream_id, &line, None, false)?;\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 42|    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n*43|    NetStreamSubscribeReq, NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, NetStreamUnsubscribeReq, NetStreamUnsubscribed, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n 44|    SessionsReply, SizeEvent, SpawnConflict, SpawnReq, Spawned, StreamLifetime, SubscribeOutcome,\n 45|    SubscribeReq, SubscribedReply, 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...\n 547|                session_id: sid,\n*548|                data_b64: encode_bytes(bytes),\n 549|                op_id: None,\n 550|                minter: None,\n 551|                ack: true,\n...\n 597|                session_id: sid,\n*598|                data_b64: encode_bytes(bytes),\n 599|                op_id: Some(op.seq),\n 600|                minter: Some(op.minter),\n 601|                ack,\n...\n 690|                KIND_OUTPUT => {\n*691|                    let ev: OutputEvent = serde_json::from_value(env.payload)\n 692|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 693|                    // A **resume-mode** brain (the per-session map is populated by\n 694|                    // `resume_sessions`) routes EVERY session through the map —\n...\n 714|                        *cursor = ev.seq + 1;\n*715|                        let bytes = decode_bytes(&ev.data_b64)\n 716|                            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 717|                        return Ok(BrokerEvent::Output {\n 718|                            session_id: ev.session_id,\n...\n 747|                    self.next_seq = ev.seq + 1;\n*748|                    let bytes = decode_bytes(&ev.data_b64)\n 749|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 750|                    return Ok(BrokerEvent::Output {\n 751|                        session_id: ev.session_id,\n...\n 885|                    *cursor = ev.seq + 1;\n*886|                    let bytes = decode_bytes(&ev.data_b64)\n 887|                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 888|                    return Ok(BrokerEvent::NetStreamData {\n 889|                        stream_id: ev.stream_id,\n...\n 1487|                stream_id,\n*1488|                data_b64: encode_bytes(bytes),\n 1489|                op_id: op.map(|o| o.seq),\n 1490|                minter: op.map(|o| o.minter),\n 1491|                finish,\n...\n 1614|                    return match reply.opener_b64 {\n*1615|                        Some(b64) => decode_bytes(&b64)\n 1616|                            .map(Some)\n 1617|                            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)),\n 1618|                        None => Ok(None),\n...\n 1677|                endpoint: endpoint.to_string(),\n*1678|                data_b64: encode_bytes(bytes),\n 1679|                native,\n 1680|            })\n 1681|            .expect(\"EndpointInputReq serializes\"),\n...\n 1963|    use super::*;\n*1964|    use crate::msg::output_envelope;\n 1965|    use crate::transport::DaemonTransport;\n 1966|    use std::sync::atomic::{AtomicU32, Ordering};\n 1967|    use std::sync::mpsc::{sync_channel, SyncSender};\n...\n 2086|        // Contiguous seq 0 is accepted and advances the cursor.\n*2087|        stub.feed.send(output_envelope(sid, 0, b\"hello\")).unwrap();\n 2088|        match brain.read_event().expect(\"seq 0 accepted\") {\n 2089|            BrokerEvent::Output { seq, bytes, .. } => {\n 2090|                assert_eq!(seq, 0);\n...\n 2097|        // path — a cold brain treats a skipped seq as a lost chunk, never silently.\n*2098|        stub.feed.send(output_envelope(sid, 5, b\"jumped\")).unwrap();\n 2099|        let err = brain.read_event().expect_err(\"a cold brain rejects a forward gap\");\n 2100|        assert_eq!(err.kind(), io::ErrorKind::InvalidData);\n 2101|        assert!(\n...\n 2142|            stub.feed\n*2143|                .send(output_envelope(sid, seq, format!(\"c{seq}\").as_bytes()))\n 2144|                .unwrap();\n 2145|            match brain.read_event().expect(\"contiguous controller frame accepted\") {\n 2146|                BrokerEvent::Output { seq: got, .. } => assert_eq!(got, seq),\n...\n 2159|        // frozen `delivered_through`); this IS the resume floor.\n*2160|        stub.feed.send(output_envelope(sid, 10, b\"gapped\")).unwrap();\n 2161|        let err = brain\n 2162|            .read_event()\n 2163|            .expect_err(\"the strict controller path fatals a forward gap\");\n...\n 2183|            stub.feed\n*2184|                .send(output_envelope(sid, seq, format!(\"r{seq}\").as_bytes()))\n 2185|                .unwrap();\n 2186|            match brain.read_event().expect(\"ring-replayed floor frame accepted\") {\n 2187|                BrokerEvent::Output { seq: got, .. } => assert_eq!(got, seq),\n...\n 2205|            .expect(\"controller second resume\");\n*2206|        stub.feed.send(output_envelope(sid, 9, b\"rolled\")).unwrap();\n 2207|        let err = brain\n 2208|            .read_event()\n 2209|            .expect_err(\"the ring rolled past the floor — still a gap\");\n...\n 2257|        // jump is only ever a legitimate post-eviction ring-floor clamp).\n*2258|        stub.feed.send(output_envelope(sid, 12, b\"LIVE-A\")).unwrap();\n 2259|        match brain.read_event().expect(\"post-eviction forward jump accepted\") {\n 2260|            BrokerEvent::Output { seq, bytes, .. } => {\n 2261|                assert_eq!(seq, 12, \"the forward-jumped live seq is accepted, not rejected\");\n...\n 2273|        // re-send (seq 12, already delivered) is DEDUPED (not a gap, not re-emitted).\n*2274|        stub.feed.send(output_envelope(sid, 13, b\"LIVE-B\")).unwrap();\n### msg.rs\n 493|    /// The raw input bytes, base64-encoded.\n*494|    pub data_b64: String,\n 495|    /// The durable op id for exactly-once delivery (B5). `Some(id)` routes the\n 496|    /// write through the broker's [`crate::effect::EffectJournal`]: a replay of\n 497|    /// the same `(session_id, id)` after a brain crash is deduped, and the broker\n...\n 548|#[derive(Debug, Clone, Serialize, Deserialize)]\n*549|pub struct OutputEvent {\n 550|    /// Originating session.\n 551|    pub session_id: u64,\n 552|    /// The broker-assigned, per-session monotonic sequence of this chunk. The\n...\n 556|    /// The raw output bytes, base64-encoded.\n*557|    pub data_b64: String,\n 558|}\n 559|\n 560|/// `exit` event — a session's child has been reaped.\n...\n 838|    /// The bytes, base64.\n*839|    pub data_b64: String,\n 840|    /// Durable op id for exactly-once delivery; `None` writes directly (no\n 841|    /// dedup, no ack) — mirrors [`InputReq::op_id`].\n 842|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n...\n 1050|    /// The raw bytes to inject (a rendered `<EVENT>` envelope), base64-encoded.\n*1051|    pub data_b64: String,\n 1052|    /// NATIVE channel restriction (ADR-0028 W3): when `true` the send specified\n 1053|    /// `--prefer-native`/`--force-native`, so the translation binary delivers\n 1054|    /// REGARDLESS of the `.idle` activity gate — the WINDOW dictates *when*, the\n...\n 1111|    /// The chunk bytes, base64.\n*1112|    pub data_b64: String,\n 1113|}\n 1114|\n 1115|/// `net-stream-eof` event — a stream's read side finished cleanly.\n...\n 1164|/// Encode raw bytes for an `input`/`output` payload.\n*1165|pub fn encode_bytes(bytes: &[u8]) -> String {\n 1166|    B64.encode(bytes)\n 1167|}\n 1168|\n 1169|/// Decode an `input`/`output` payload's base64 back to raw bytes.\n*1170|pub fn decode_bytes(s: &str) -> Result<Vec<u8>, base64::DecodeError> {\n 1171|    B64.decode(s)\n 1172|}\n 1173|\n 1174|/// Build an output event envelope for `session_id` / `seq` carrying `bytes`.\n*1175|pub fn output_envelope(session_id: u64, seq: u64, bytes: &[u8]) -> Envelope {\n 1176|    Envelope::new(\n 1177|        KIND_OUTPUT,\n*1178|        serde_json::to_value(OutputEvent {\n 1179|            session_id,\n 1180|            seq,\n*1181|            data_b64: encode_bytes(bytes),\n 1182|        })\n*1183|        .expect(\"OutputEvent serializes\"),\n 1184|    )\n 1185|}\n 1186|\n...\n 1269|/// Build a `net-stream-data` event envelope (D4b) — the stream-read sibling of\n*1270|/// [`output_envelope`].\n 1271|pub fn net_stream_data_envelope(stream_id: u64, seq: u64, bytes: &[u8]) -> Envelope {\n 1272|    Envelope::new(\n 1273|        KIND_NET_STREAM_DATA,\n...\n 1276|            seq,\n*1277|            data_b64: encode_bytes(bytes),\n 1278|        })\n 1279|        .expect(\"NetStreamData serializes\"),\n 1280|    )\n...\n 1337|        let raw = b\"\\x1b[6n binary\\x00\\xff bytes\";\n*1338|        let enc = encode_bytes(raw);\n*1339|        assert_eq!(decode_bytes(&enc).unwrap(), raw);\n 1340|    }\n 1341|\n 1342|    // [unit->REQ-STREAM-LEASE-CLASSES] the lifetime class is ADDITIVE on the\n...\n 1516|            endpoint: \"wall-b\".into(),\n*1517|            data_b64: encode_bytes(b\"<EVENT type=\\\"msg\\\" from=\\\"doyle\\\">hi</EVENT>\\n\"),\n 1518|            native: false,\n 1519|        };\n 1520|        let back: EndpointInputReq =\n...\n 1522|        assert_eq!(back.endpoint, \"wall-b\");\n*1523|        assert_eq!(decode_bytes(&back.data_b64).unwrap(), decode_bytes(&req.data_b64).unwrap());\n 1524|\n 1525|        let env = endpoint_injected_envelope(\"wall-b\", true, false);\n 1526|        assert_eq!(env.kind, KIND_ENDPOINT_INJECTED);\n...\n 1568|    #[test]\n*1569|    fn output_envelope_round_trips() {\n*1570|        let env = output_envelope(7, 42, b\"hello\");\n 1571|        assert_eq!(env.kind, KIND_OUTPUT);\n### broker.rs\n 53|use crate::msg::{\n*54|    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,\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...\n 915|        }\n*916|        let frame = output_envelope(self.session_id, seq, bytes);\n 917|        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\n 918|        if !self.viewers.is_empty() {\n 919|            let mut evicted: Vec<u64> = Vec::new();\n...\n 1231|        match self.repaint_initial(from_seq) {\n*1232|            Some((seq, bytes)) => initial.push(output_envelope(self.session_id, seq, &bytes)),\n 1233|            None => initial.extend(\n 1234|                self.ring\n 1235|                    .iter()\n 1236|                    .filter(|(s, _)| *s >= from_seq)\n*1237|                    .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),\n 1238|            ),\n 1239|        }\n 1240|        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);\n...\n 1691|    for (seq, bytes) in &initial {\n*1692|        let frame = output_envelope(sid, *seq, bytes);\n 1693|        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n 1694|        // epoch past ours) must write NOTHING further — otherwise its owned\n 1695|        // initial batch races the new writer on the same socket and reorders the\n...\n 3922|            serde_json::from_value(env.payload).map_err(|e| format!(\"bad input payload: {e}\"))?;\n*3923|        let bytes = decode_bytes(&req.data_b64).map_err(|e| format!(\"bad input base64: {e}\"))?;\n 3924|        // Clone the input-writer Arc (and the translation handle, if any) out from\n 3925|        // under the sessions lock so the ENQUEUE happens without holding the table\n 3926|        // lock (mirrors dispatch_subscribe). The enqueue is non-blocking, so the\n...\n 4094|        let bytes =\n*4095|            decode_bytes(&req.data_b64).map_err(|e| format!(\"bad endpoint-input base64: {e}\"))?;\n 4096|        // Resolve endpoint → translation under the lock, cloning the Arc out so the\n 4097|        // event send happens without holding the table lock (dispatch_input rule).\n 4098|        // The session's PTY input writer is intentionally NOT pulled here: idle\n...\n 4686|        let bytes =\n*4687|            decode_bytes(&req.data_b64).map_err(|e| format!(\"bad net-stream-send base64: {e}\"))?;\n 4688|        let host = self.net.get().ok_or(\"net disabled\")?;\n 4689|        match req.op_id {\n 4690|            None => host\n...\n 4809|                stream_id: req.stream_id,\n*4810|                opener_b64: opener.map(|line| crate::msg::encode_bytes(&line)),\n 4811|            })\n 4812|            .expect(\"NetStreamOpenerReply serializes\"),\n 4813|        );\n...\n 6073|            if env.kind == crate::msg::KIND_OUTPUT {\n*6074|                let ev: crate::msg::OutputEvent =\n 6075|                    serde_json::from_value(env.payload).expect(\"output payload\");\n 6076|                seqs.push(ev.seq);\n 6077|            }\n...\n 6379|        assert_eq!(frame.kind, crate::msg::KIND_OUTPUT, \"the initial batch is an output frame\");\n*6380|        let ev: crate::msg::OutputEvent =\n 6381|            serde_json::from_value(frame.payload).expect(\"output payload\");\n*6382|        let repaint = decode_bytes(&ev.data_b64).expect(\"repaint b64 decodes\");\n 6383|        let s = String::from_utf8_lossy(&repaint);\n 6384|\n 6385|        assert!(\n\n## spt-net/src/net/\n### attach.rs\n 9|//! sends [`AttachRecord::Request`], and the target node pumps its session's\n*10|//! `OutputLog` chunks back as [`AttachRecord::Output`] while injecting\n 11|//! [`AttachRecord::Input`] into the PTY through the journaled exactly-once\n 12|//! input path.\n 13|//!\n...\n 15|//! base64 inside the JSON (the same posture as the IPC catalog's\n*16|//! `data_b64` fields — no binary fork of the codec). `Output.seq` is the\n 17|//! target session's `OutputLog` sequence verbatim, so the operator side dedups\n 18|//! replays (a target-brain restart redrives journaled sends) by cursor, and a\n 19|//! restarted operator brain resubscribes the QUIC stream and skips what it\n...\n 105|    /// session's `OutputLog` sequence verbatim (the operator's dedup cursor).\n*106|    Output { seq: u64, data_b64: String },\n 107|    /// Operator → target: input bytes for the session's PTY. `op_id` is the\n 108|    /// operator-minted durable id the target feeds into its journaled\n 109|    /// exactly-once input path (a replayed record cannot double-type). Honored\n 110|    /// only from the CONTROLLER's stream (a viewer sends none).\n*111|    Input { data_b64: String, op_id: u64 },\n 112|    /// Operator(controller) → target: resize the PTY to the controller's viewport.\n 113|    /// Controller-EXCLUSIVE — the broker rejects a `Resize` arriving on a viewer's\n 114|    /// stream (REQ-RCVIEW-1, resize is controller-exclusive for ConPTY repaint cost).\n...\n 155|             gen: 0, },\n*156|            AttachRecord::Output {\n 157|                seq: 3,\n*158|                data_b64: \"aGk=\".into(),\n 159|            },\n 160|            AttachRecord::Input {\n*161|                data_b64: \"bHM=\".into(),\n 162|                op_id: 42,\n 163|            },\n 164|            AttachRecord::Resize { rows: 40, cols: 120 },\n...\n 193|    fn record_split_across_chunks_reassembles() {\n*194|        let r = AttachRecord::Output {\n 195|            seq: 9,\n*196|            data_b64: \"c3BsaXQtbWU=\".into(),\n 197|        };\n 198|        let line = r.encode_line();\n 199|        let mut dec = AttachDecoder::new();"}],"details":{"scopePath":"crates/spt/src/rc.rs, crates/spt-daemon/src/attach.rs, crates/spt-daemon/src/brain.rs, crates/spt-daemon/src/msg.rs, crates/spt-daemon/src/broker.rs, crates/spt-net/src/net/attach.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":76,"fileCount":6,"files":["crates/spt/src/rc.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/broker.rs","crates/spt-net/src/net/attach.rs"],"fileMatches":[{"path":"crates/spt/src/rc.rs","count":4},{"path":"crates/spt-daemon/src/attach.rs","count":10},{"path":"crates/spt-daemon/src/brain.rs","count":20},{"path":"crates/spt-daemon/src/msg.rs","count":20},{"path":"crates/spt-daemon/src/broker.rs","count":13},{"path":"crates/spt-net/src/net/attach.rs","count":9}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt/src/\n### rc.rs\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     │...\n 1837│/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n*1838│/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n 1839│#[allow(clippy::too_many_arguments)]\n 1840│fn pump(\n 1841│    brain: &mut Brain,\n     │...\n 1965│                    match rec {\n*1966│                        AttachRecord::Output { seq, data_b64 } => {\n 1967│                            if seq < cursor {\n 1968│                                continue; // already rendered — re-serve dedup\n 1969│                            }\n     │...\n 1971│                            rendered_any = true;\n*1972│                            let bytes = decode_bytes(&data_b64)\n 1973│                                .map_err(|e| format!(\"decode output: {e}\"))?;\n 1974│                            // Track the harness's mouse-reporting mode from its output\n 1975│                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.\n\n## spt-daemon/src/\n### attach.rs\n   7│//!\n  *8│//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire\n   9│//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);\n  10│//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the\n  11│//!   operator-minted `op_id` — exactly-once at the PTY write through the B5\n    │...\n  52│use crate::effect::{Minter, MintedOp};\n *53│use crate::msg::{decode_bytes, encode_bytes};\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    │...\n 413│                            b\"\\r\\n[spt: output truncated -- controller fell behind the live ring, frames lost]\\r\\n\";\n*414│                        let line = ndjson::encode_line(&AttachRecord::Output {\n 415│                            seq: brain.controller_resume_floor(),\n*416│                            data_b64: encode_bytes(notice),\n 417│                        });\n 418│                        let _ = wire.net_stream_send(stream_id, &line, None, true);\n 419│                        feed_rest(endpoint, crate::resting::RestEvent::Detach);\n    │...\n 468│                        }\n*469│                        AttachRecord::Input { data_b64, op_id }\n 470│                            if attached && intent != AttachIntent::Viewer =>\n 471│                        {\n*472│                            let bytes = decode_bytes(&data_b64)\n 473│                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n 474│                            match role {\n 475│                                // Confirmed controller — exactly-once at the PTY\n    │...\n 600│            BrokerEvent::Output { seq, bytes, .. } if attached => {\n*601│                let line = ndjson::encode_line(&AttachRecord::Output {\n 602│                    seq,\n*603│                    data_b64: encode_bytes(&bytes),\n 604│                });\n 605│                // Unjournaled by design (see module docs): `seq` rides inside\n 606│                // the record, and the operator's render cursor is the dedup.\n    │...\n 631│/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\n*632│/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\n 633│/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\n 634│/// durable id for the exactly-once stream open (safe to block for its ack —\n 635│/// no attach traffic can be in flight before the stream exists); the Request\n    │...\n 721│    let line = ndjson::encode_line(&AttachRecord::Input {\n*722│        data_b64: encode_bytes(bytes),\n 723│        op_id: wire_op,\n 724│    });\n 725│    brain.net_stream_send(stream_id, &line, None, false)?;\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   42│    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,\n  *43│    NetStreamSubscribeReq, NetStreamsReply, NetStreamOpenerReply, NetStreamOpenerReq, NetStreamRetireReq, NetStreamRetired, NetStreamUnsubscribeReq, NetStreamUnsubscribed, OutputEvent, MetMember, PairCodeSubmit, PairJoinReply, PairJoinReq, PairMeetReq, ResizeReq,\n   44│    SessionsReply, SizeEvent, SpawnConflict, SpawnReq, Spawned, StreamLifetime, SubscribeOutcome,\n   45│    SubscribeReq, SubscribedReply, 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     │...\n  547│                session_id: sid,\n *548│                data_b64: encode_bytes(bytes),\n  549│                op_id: None,\n  550│                minter: None,\n  551│                ack: true,\n     │...\n  597│                session_id: sid,\n *598│                data_b64: encode_bytes(bytes),\n  599│                op_id: Some(op.seq),\n  600│                minter: Some(op.minter),\n  601│                ack,\n     │...\n  690│                KIND_OUTPUT => {\n *691│                    let ev: OutputEvent = serde_json::from_value(env.payload)\n  692│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n  693│                    // A **resume-mode** brain (the per-session map is populated by\n  694│                    // `resume_sessions`) routes EVERY session through the map —\n     │...\n  714│                        *cursor = ev.seq + 1;\n *715│                        let bytes = decode_bytes(&ev.data_b64)\n  716│                            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n  717│                        return Ok(BrokerEvent::Output {\n  718│                            session_id: ev.session_id,\n     │...\n  747│                    self.next_seq = ev.seq + 1;\n *748│                    let bytes = decode_bytes(&ev.data_b64)\n  749│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n  750│                    return Ok(BrokerEvent::Output {\n  751│                        session_id: ev.session_id,\n     │...\n  885│                    *cursor = ev.seq + 1;\n *886│                    let bytes = decode_bytes(&ev.data_b64)\n  887│                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n  888│                    return Ok(BrokerEvent::NetStreamData {\n  889│                        stream_id: ev.stream_id,\n     │...\n 1487│                stream_id,\n*1488│                data_b64: encode_bytes(bytes),\n 1489│                op_id: op.map(|o| o.seq),\n 1490│                minter: op.map(|o| o.minter),\n 1491│                finish,\n     │...\n 1614│                    return match reply.opener_b64 {\n*1615│                        Some(b64) => decode_bytes(&b64)\n 1616│                            .map(Some)\n 1617│                            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)),\n 1618│                        None => Ok(None),\n     │...\n 1677│                endpoint: endpoint.to_string(),\n*1678│                data_b64: encode_bytes(bytes),\n 1679│                native,\n 1680│            })\n 1681│            .expect(\"EndpointInputReq serializes\"),\n     │...\n 1963│    use super::*;\n*1964│    use crate::msg::output_envelope;\n 1965│    use crate::transport::DaemonTransport;\n 1966│    use std::sync::atomic::{AtomicU32, Ordering};\n 1967│    use std::sync::mpsc::{sync_channel, SyncSender};\n     │...\n 2086│        // Contiguous seq 0 is accepted and advances the cursor.\n*2087│        stub.feed.send(output_envelope(sid, 0, b\"hello\")).unwrap();\n 2088│        match brain.read_event().expect(\"seq 0 accepted\") {\n 2089│            BrokerEvent::Output { seq, bytes, .. } => {\n 2090│                assert_eq!(seq, 0);\n     │...\n 2097│        // path — a cold brain treats a skipped seq as a lost chunk, never silently.\n*2098│        stub.feed.send(output_envelope(sid, 5, b\"jumped\")).unwrap();\n 2099│        let err = brain.read_event().expect_err(\"a cold brain rejects a forward gap\");\n 2100│        assert_eq!(err.kind(), io::ErrorKind::InvalidData);\n 2101│        assert!(\n     │...\n 2142│            stub.feed\n*2143│                .send(output_envelope(sid, seq, format!(\"c{seq}\").as_bytes()))\n 2144│                .unwrap();\n 2145│            match brain.read_event().expect(\"contiguous controller frame accepted\") {\n 2146│                BrokerEvent::Output { seq: got, .. } => assert_eq!(got, seq),\n     │...\n 2159│        // frozen `delivered_through`); this IS the resume floor.\n*2160│        stub.feed.send(output_envelope(sid, 10, b\"gapped\")).unwrap();\n 2161│        let err = brain\n 2162│            .read_event()\n 2163│            .expect_err(\"the strict controller path fatals a forward gap\");\n     │...\n 2183│            stub.feed\n*2184│                .send(output_envelope(sid, seq, format!(\"r{seq}\").as_bytes()))\n 2185│                .unwrap();\n 2186│            match brain.read_event().expect(\"ring-replayed floor frame accepted\") {\n 2187│                BrokerEvent::Output { seq: got, .. } => assert_eq!(got, seq),\n     │...\n 2205│            .expect(\"controller second resume\");\n*2206│        stub.feed.send(output_envelope(sid, 9, b\"rolled\")).unwrap();\n 2207│        let err = brain\n 2208│            .read_event()\n 2209│            .expect_err(\"the ring rolled past the floor — still a gap\");\n     │...\n 2257│        // jump is only ever a legitimate post-eviction ring-floor clamp).\n*2258│        stub.feed.send(output_envelope(sid, 12, b\"LIVE-A\")).unwrap();\n 2259│        match brain.read_event().expect(\"post-eviction forward jump accepted\") {\n 2260│            BrokerEvent::Output { seq, bytes, .. } => {\n 2261│                assert_eq!(seq, 12, \"the forward-jumped live seq is accepted, not rejected\");\n     │...\n 2273│        // re-send (seq 12, already delivered) is DEDUPED (not a gap, not re-emitted).\n*2274│        stub.feed.send(output_envelope(sid, 13, b\"LIVE-B\")).unwrap();\n### msg.rs\n  493│    /// The raw input bytes, base64-encoded.\n *494│    pub data_b64: String,\n  495│    /// The durable op id for exactly-once delivery (B5). `Some(id)` routes the\n  496│    /// write through the broker's [`crate::effect::EffectJournal`]: a replay of\n  497│    /// the same `(session_id, id)` after a brain crash is deduped, and the broker\n     │...\n  548│#[derive(Debug, Clone, Serialize, Deserialize)]\n *549│pub struct OutputEvent {\n  550│    /// Originating session.\n  551│    pub session_id: u64,\n  552│    /// The broker-assigned, per-session monotonic sequence of this chunk. The\n     │...\n  556│    /// The raw output bytes, base64-encoded.\n *557│    pub data_b64: String,\n  558│}\n  559│\n  560│/// `exit` event — a session's child has been reaped.\n     │...\n  838│    /// The bytes, base64.\n *839│    pub data_b64: String,\n  840│    /// Durable op id for exactly-once delivery; `None` writes directly (no\n  841│    /// dedup, no ack) — mirrors [`InputReq::op_id`].\n  842│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n     │...\n 1050│    /// The raw bytes to inject (a rendered `<EVENT>` envelope), base64-encoded.\n*1051│    pub data_b64: String,\n 1052│    /// NATIVE channel restriction (ADR-0028 W3): when `true` the send specified\n 1053│    /// `--prefer-native`/`--force-native`, so the translation binary delivers\n 1054│    /// REGARDLESS of the `.idle` activity gate — the WINDOW dictates *when*, the\n     │...\n 1111│    /// The chunk bytes, base64.\n*1112│    pub data_b64: String,\n 1113│}\n 1114│\n 1115│/// `net-stream-eof` event — a stream's read side finished cleanly.\n     │...\n 1164│/// Encode raw bytes for an `input`/`output` payload.\n*1165│pub fn encode_bytes(bytes: &[u8]) -> String {\n 1166│    B64.encode(bytes)\n 1167│}\n 1168│\n 1169│/// Decode an `input`/`output` payload's base64 back to raw bytes.\n*1170│pub fn decode_bytes(s: &str) -> Result<Vec<u8>, base64::DecodeError> {\n 1171│    B64.decode(s)\n 1172│}\n 1173│\n 1174│/// Build an output event envelope for `session_id` / `seq` carrying `bytes`.\n*1175│pub fn output_envelope(session_id: u64, seq: u64, bytes: &[u8]) -> Envelope {\n 1176│    Envelope::new(\n 1177│        KIND_OUTPUT,\n*1178│        serde_json::to_value(OutputEvent {\n 1179│            session_id,\n 1180│            seq,\n*1181│            data_b64: encode_bytes(bytes),\n 1182│        })\n*1183│        .expect(\"OutputEvent serializes\"),\n 1184│    )\n 1185│}\n 1186│\n     │...\n 1269│/// Build a `net-stream-data` event envelope (D4b) — the stream-read sibling of\n*1270│/// [`output_envelope`].\n 1271│pub fn net_stream_data_envelope(stream_id: u64, seq: u64, bytes: &[u8]) -> Envelope {\n 1272│    Envelope::new(\n 1273│        KIND_NET_STREAM_DATA,\n     │...\n 1276│            seq,\n*1277│            data_b64: encode_bytes(bytes),\n 1278│        })\n 1279│        .expect(\"NetStreamData serializes\"),\n 1280│    )\n     │...\n 1337│        let raw = b\"\\x1b[6n binary\\x00\\xff bytes\";\n*1338│        let enc = encode_bytes(raw);\n*1339│        assert_eq!(decode_bytes(&enc).unwrap(), raw);\n 1340│    }\n 1341│\n 1342│    // [unit->REQ-STREAM-LEASE-CLASSES] the lifetime class is ADDITIVE on the\n     │...\n 1516│            endpoint: \"wall-b\".into(),\n*1517│            data_b64: encode_bytes(b\"<EVENT type=\\\"msg\\\" from=\\\"doyle\\\">hi</EVENT>\\n\"),\n 1518│            native: false,\n 1519│        };\n 1520│        let back: EndpointInputReq =\n     │...\n 1522│        assert_eq!(back.endpoint, \"wall-b\");\n*1523│        assert_eq!(decode_bytes(&back.data_b64).unwrap(), decode_bytes(&req.data_b64).unwrap());\n 1524│\n 1525│        let env = endpoint_injected_envelope(\"wall-b\", true, false);\n 1526│        assert_eq!(env.kind, KIND_ENDPOINT_INJECTED);\n     │...\n 1568│    #[test]\n*1569│    fn output_envelope_round_trips() {\n*1570│        let env = output_envelope(7, 42, b\"hello\");\n 1571│        assert_eq!(env.kind, KIND_OUTPUT);\n### broker.rs\n   53│use crate::msg::{\n  *54│    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,\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     │...\n  915│        }\n *916│        let frame = output_envelope(self.session_id, seq, bytes);\n  917│        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.\n  918│        if !self.viewers.is_empty() {\n  919│            let mut evicted: Vec<u64> = Vec::new();\n     │...\n 1231│        match self.repaint_initial(from_seq) {\n*1232│            Some((seq, bytes)) => initial.push(output_envelope(self.session_id, seq, &bytes)),\n 1233│            None => initial.extend(\n 1234│                self.ring\n 1235│                    .iter()\n 1236│                    .filter(|(s, _)| *s >= from_seq)\n*1237│                    .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),\n 1238│            ),\n 1239│        }\n 1240│        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);\n     │...\n 1691│    for (seq, bytes) in &initial {\n*1692│        let frame = output_envelope(sid, *seq, bytes);\n 1693│        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared\n 1694│        // epoch past ours) must write NOTHING further — otherwise its owned\n 1695│        // initial batch races the new writer on the same socket and reorders the\n     │...\n 3922│            serde_json::from_value(env.payload).map_err(|e| format!(\"bad input payload: {e}\"))?;\n*3923│        let bytes = decode_bytes(&req.data_b64).map_err(|e| format!(\"bad input base64: {e}\"))?;\n 3924│        // Clone the input-writer Arc (and the translation handle, if any) out from\n 3925│        // under the sessions lock so the ENQUEUE happens without holding the table\n 3926│        // lock (mirrors dispatch_subscribe). The enqueue is non-blocking, so the\n     │...\n 4094│        let bytes =\n*4095│            decode_bytes(&req.data_b64).map_err(|e| format!(\"bad endpoint-input base64: {e}\"))?;\n 4096│        // Resolve endpoint → translation under the lock, cloning the Arc out so the\n 4097│        // event send happens without holding the table lock (dispatch_input rule).\n 4098│        // The session's PTY input writer is intentionally NOT pulled here: idle\n     │...\n 4686│        let bytes =\n*4687│            decode_bytes(&req.data_b64).map_err(|e| format!(\"bad net-stream-send base64: {e}\"))?;\n 4688│        let host = self.net.get().ok_or(\"net disabled\")?;\n 4689│        match req.op_id {\n 4690│            None => host\n     │...\n 4809│                stream_id: req.stream_id,\n*4810│                opener_b64: opener.map(|line| crate::msg::encode_bytes(&line)),\n 4811│            })\n 4812│            .expect(\"NetStreamOpenerReply serializes\"),\n 4813│        );\n     │...\n 6073│            if env.kind == crate::msg::KIND_OUTPUT {\n*6074│                let ev: crate::msg::OutputEvent =\n 6075│                    serde_json::from_value(env.payload).expect(\"output payload\");\n 6076│                seqs.push(ev.seq);\n 6077│            }\n     │...\n 6379│        assert_eq!(frame.kind, crate::msg::KIND_OUTPUT, \"the initial batch is an output frame\");\n*6380│        let ev: crate::msg::OutputEvent =\n 6381│            serde_json::from_value(frame.payload).expect(\"output payload\");\n*6382│        let repaint = decode_bytes(&ev.data_b64).expect(\"repaint b64 decodes\");\n 6383│        let s = String::from_utf8_lossy(&repaint);\n 6384│\n 6385│        assert!(\n\n## spt-net/src/net/\n### attach.rs\n   9│//! sends [`AttachRecord::Request`], and the target node pumps its session's\n *10│//! `OutputLog` chunks back as [`AttachRecord::Output`] while injecting\n  11│//! [`AttachRecord::Input`] into the PTY through the journaled exactly-once\n  12│//! input path.\n  13│//!\n    │...\n  15│//! base64 inside the JSON (the same posture as the IPC catalog's\n *16│//! `data_b64` fields — no binary fork of the codec). `Output.seq` is the\n  17│//! target session's `OutputLog` sequence verbatim, so the operator side dedups\n  18│//! replays (a target-brain restart redrives journaled sends) by cursor, and a\n  19│//! restarted operator brain resubscribes the QUIC stream and skips what it\n    │...\n 105│    /// session's `OutputLog` sequence verbatim (the operator's dedup cursor).\n*106│    Output { seq: u64, data_b64: String },\n 107│    /// Operator → target: input bytes for the session's PTY. `op_id` is the\n 108│    /// operator-minted durable id the target feeds into its journaled\n 109│    /// exactly-once input path (a replayed record cannot double-type). Honored\n 110│    /// only from the CONTROLLER's stream (a viewer sends none).\n*111│    Input { data_b64: String, op_id: u64 },\n 112│    /// Operator(controller) → target: resize the PTY to the controller's viewport.\n 113│    /// Controller-EXCLUSIVE — the broker rejects a `Resize` arriving on a viewer's\n 114│    /// stream (REQ-RCVIEW-1, resize is controller-exclusive for ConPTY repaint cost).\n    │...\n 155│             gen: 0, },\n*156│            AttachRecord::Output {\n 157│                seq: 3,\n*158│                data_b64: \"aGk=\".into(),\n 159│            },\n 160│            AttachRecord::Input {\n*161│                data_b64: \"bHM=\".into(),\n 162│                op_id: 42,\n 163│            },\n 164│            AttachRecord::Resize { rows: 40, cols: 120 },\n    │...\n 193│    fn record_split_across_chunks_reassembles() {\n*194│        let r = AttachRecord::Output {\n 195│            seq: 9,\n*196│            data_b64: \"c3BsaXQtbWU=\".into(),\n 197│        };\n 198│        let line = r.encode_line();\n 199│        let mut dec = AttachDecoder::new();"},"isError":false,"timestamp":1784365505175}}
{"type":"message","id":"fc04909a","parentId":"340d2bb9","timestamp":"2026-07-18T09:05:05.180Z","message":{"role":"toolResult","toolCallId":"call_cozkvEQP2IDSg4zz1tIfEAw8|fc_0ce5441d29b9f05d016a5b41c0fc20819b90c510cea2778c32","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/src/\n### rc.rs\n 43|/// drain stdin — the full-duplex interleave granularity.\n*44|const POLL_SLICE: Duration = Duration::from_millis(40);\n 45|\n 46|/// A message from the stdin reader thread to the pump.\n 47|enum StdinMsg {\n...\n 1189|    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n*1190|    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n 1191|    // keeps today's clean-exit copy on a no-target miss.\n 1192|    let est = match establish_attach(endpoint_id, intent) {\n 1193|        Ok(est) => est,\n...\n 1197|        }\n*1198|        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n 1199|        // rc won't birth one. Plain operator language — what's wrong, what to do —\n 1200|        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n*1201|        // [impl->REQ-RC-RECONNECT-TRUTH]\n 1202|        Err(EstablishFail::DaemonDown) => {\n 1203|            println!(\n 1204|                \"The spt daemon isn't running — nothing to attach to. \\\n...\n 1279|        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n*1280|            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n 1281|            // successful re-attach. Plain operator language (Q4 UX rule): what\n 1282|            // happened, what it means, what to do next — never op/read-err lingo.\n 1283|            if daemon_down {\n*1284|                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n 1285|                // went down and never came back — rc did NOT resurrect it. Name that\n 1286|                // plainly so the operator restarts the daemon rather than guessing.\n 1287|                let _ = writeln!(\n...\n 1291|                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n*1292|                    RECONNECT_WINDOW.as_secs()\n 1293|                );\n 1294|            } else {\n 1295|                let _ = writeln!(\n...\n 1299|                     {endpoint_id}` again once the node/daemon is back]\",\n*1300|                    RECONNECT_WINDOW.as_secs()\n 1301|                );\n 1302|            }\n 1303|            Ok(())\n...\n 1345|/// F-1 (REQ-PUBLIC-ERROR-SURFACES): the residual attach-failure line is the one\n*1346|/// rc surface that still leaked transport lingo (\"brain IPC read deadline\n 1347|/// elapsed\") to an operator. Map the known daemon-didn't-answer class to the\n 1348|/// observable situation + next action; every OTHER error passes through\n 1349|/// verbatim (no over-matching — unknown failures keep their detail). Pure, so\n...\n 1352|fn public_attach_failure(endpoint_id: &str, e: String) -> String {\n*1353|    if e.contains(\"brain IPC read deadline\") {\n 1354|        format!(\n 1355|            \"the daemon didn't answer in time while attaching to '{endpoint_id}' — \\\n 1356|             it may be busy or mid-restart. Try `spt rc {endpoint_id}` again; if it \\\n...\n 1367|/// not just the stream; a fresh OpMinter keeps ADR-0034 `rc` op tracing per\n*1368|/// viewport). [impl->REQ-RC-RECONNECT]\n 1369|struct Established {\n 1370|    brain: Brain,\n 1371|    ops: OpMinter,\n...\n 1385|    /// The daemon isn't running — rc is CONNECT-ONLY and refuses to auto-start it\n*1386|    /// (REQ-RC-RECONNECT-TRUTH). On the initial attach this is a loud \"daemon\n 1387|    /// isn't running — start it\" exit; inside the reconnect loop it's just another\n 1388|    /// retry (the operator may bring the daemon back), never a resurrection.\n*1389|    /// [impl->REQ-RC-RECONNECT-TRUTH]\n 1390|    DaemonDown,\n 1391|    /// A setup error (broker connect / dial / attach-open / subscribe).\n 1392|    Error(String),\n...\n 1397|/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n*1398|/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n 1399|///\n 1400|/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n 1401|/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n...\n 1415|/// a hang (D2/D6).\n*1416|// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\n 1417|fn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n*1418|    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n 1419|    // launcher. It must NEVER auto-start the broker — the historical\n 1420|    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n 1421|    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n...\n 1424|    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n*1425|    // broker cold-connect below is already deadline-bounded, so a down daemon\n 1426|    // never hangs.\n*1427|    // [impl->REQ-RC-RECONNECT-TRUTH]\n 1428|    if !spt_daemon::daemon::is_running() {\n 1429|        return Err(EstablishFail::DaemonDown);\n 1430|    }\n*1431|    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n 1432|    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n 1433|    // pipes), so the single-threaded pump interleaves stdin + broker events\n 1434|    // without wedging. The generous io_timeout bounds the setup calls (dial,\n...\n 1531|/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\n*1532|/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\n 1533|/// A severed viewport (broker bounce, remote conn drop — the reconnectable\n 1534|/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\n 1535|/// inside a bounded window; every other end is FINAL and returns to the\n\n## spt-daemon/src/\n### attach.rs\n 107|/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n*108|/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n 109|/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n 110|// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n 111|#[derive(Debug)]\n### broker.rs\n 64|    KIND_ADAPTER_APPLY, KIND_APPLIED,\n*65|    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n 66|    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n 67|    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n 68|    KIND_NET_STREAM_UNSUBSCRIBE, KIND_NET_STREAM_UNSUBSCRIBED,\n...\n 143|/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n*144|/// never trips the deadline.\n 145|const CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n 146|\n 147|/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n...\n 170|/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n*171|/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n 172|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 173|const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n 174|\n*175|/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n 176|/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n 177|/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n 178|/// the slot) is provable in ~1 s instead of 15 (mirrors the\n...\n 181|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*182|fn brain_write_deadline() -> Duration {\n 183|    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n 184|        Ok(ms) => ms\n 185|            .parse::<u64>()\n...\n 232|// [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\n*233|fn wake_gate_decision(live: bool, claimed: bool, past_deadline: bool) -> WakeGate {\n 234|    if live {\n 235|        WakeGate::AlreadyLive\n*236|    } else if claimed && !past_deadline {\n 237|        WakeGate::Racing\n 238|    } else {\n 239|        WakeGate::Claim\n...\n 242|\n*243|/// Boot grace before an adapter session with no live client chain can read as a\n 244|/// zombie (ADR-0041 decision 6): a just-spawned harness wrapper has not forked\n 245|/// its clients yet. Env-tunable so the int rig is deterministic.\n*246|fn spawn_client_grace() -> Duration {\n 247|    std::env::var(\"SPT_SPAWN_CLIENT_GRACE_MS\")\n 248|        .ok()\n 249|        .and_then(|v| v.parse::<u64>().ok())\n...\n 261|///     forks a harness client chain under the wrapper by construction), PAST\n*262|///     the boot grace, whose wrapper has NO live descendants — the deployah\n 263|///     wedge shape: the launcher shell idles on while the harness underneath\n 264|///     it died, so `run` said ALREADY_LIVE, `list` said OFFLINE, and\n 265|///     `shutdown` said NO_EDGE about the same corpse.\n...\n 274|    has_live_descendants: bool,\n*275|    past_grace: bool,\n 276|) -> bool {\n 277|    match wrapper_alive {\n 278|        None => false,        // no probeable pid — never guess\n 279|        Some(false) => true,  // dead root, surviving record — always a zombie\n*280|        Some(true) => adapter_labeled && past_grace && !has_live_descendants,\n 281|    }\n 282|}\n 283|\n...\n 296|    });\n*297|    let past_grace = Duration::from_millis(spawned_ms_ago) >= spawn_client_grace();\n*298|    zombie_verdict(wrapper_alive, adapter_labeled, has_live_descendants, past_grace)\n 299|}\n 300|\n 301|/// Whether a stall-evict re-stamps the perch INLINE (take/reattach path, already\n...\n 316|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*317|static STALL_EVICT_COUNT: AtomicU64 = AtomicU64::new(0);\n*318|static STALL_EVICT_LAST_MS: AtomicU64 = AtomicU64::new(0);\n 319|\n 320|/// Record one stall-evict in the broker-global tally (best-effort observability).\n 321|// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 322|fn record_stall_evict() {\n*323|    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n*324|    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n 325|}\n 326|\n 327|/// The current stall-evict tally `(count_since_boot, last_evict_wall_ms)` —\n...\n 332|    (\n*333|        STALL_EVICT_COUNT.load(Ordering::Relaxed),\n*334|        STALL_EVICT_LAST_MS.load(Ordering::Relaxed),\n 335|    )\n 336|}\n 337|\n### conn.rs\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...\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...\n 229|    /// Render the once-per-conn retirement record — the F-039 token split\n*230|    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231|    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232|    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233|    /// already-dead counterpart, the offline-peer dial churn) emits the\n...\n 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...\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...\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...\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 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 {"}],"details":{"scopePath":"crates/spt/src/rc.rs, crates/spt-daemon/src/attach.rs, crates/spt-daemon/src/broker.rs, crates/spt-daemon/src/conn.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":61,"fileCount":4,"files":["crates/spt/src/rc.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs"],"fileMatches":[{"path":"crates/spt/src/rc.rs","count":20},{"path":"crates/spt-daemon/src/attach.rs","count":1},{"path":"crates/spt-daemon/src/broker.rs","count":20},{"path":"crates/spt-daemon/src/conn.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt/src/\n### rc.rs\n   43│/// drain stdin — the full-duplex interleave granularity.\n  *44│const POLL_SLICE: Duration = Duration::from_millis(40);\n   45│\n   46│/// A message from the stdin reader thread to the pump.\n   47│enum StdinMsg {\n     │...\n 1189│    // subscribe). Factored out so the B-3 reconnect loop can RE-DRIVE the whole\n*1190│    // sequence after a severed viewport (REQ-RC-RECONNECT). The first attempt\n 1191│    // keeps today's clean-exit copy on a no-target miss.\n 1192│    let est = match establish_attach(endpoint_id, intent) {\n 1193│        Ok(est) => est,\n     │...\n 1197│        }\n*1198│        // CONNECT-ONLY loud exit (REQ-RC-RECONNECT-TRUTH): the daemon is down and\n 1199│        // rc won't birth one. Plain operator language — what's wrong, what to do —\n 1200│        // never the old freeze-forever \"Reconnecting…\" or a silent WMI spawn.\n*1201│        // [impl->REQ-RC-RECONNECT-TRUTH]\n 1202│        Err(EstablishFail::DaemonDown) => {\n 1203│            println!(\n 1204│                \"The spt daemon isn't running — nothing to attach to. \\\n     │...\n 1279│        Ok(PumpEnd::ReconnectGaveUp { detail, daemon_down }) => {\n*1280│            // B-3 (REQ-RC-RECONNECT): the reconnect window expired without a\n 1281│            // successful re-attach. Plain operator language (Q4 UX rule): what\n 1282│            // happened, what it means, what to do next — never op/read-err lingo.\n 1283│            if daemon_down {\n*1284│                // Loud daemon-down truth (REQ-RC-RECONNECT-TRUTH): the local daemon\n 1285│                // went down and never came back — rc did NOT resurrect it. Name that\n 1286│                // plainly so the operator restarts the daemon rather than guessing.\n 1287│                let _ = writeln!(\n     │...\n 1291│                     back with `spt daemon start`, then `spt rc {endpoint_id}`]\",\n*1292│                    RECONNECT_WINDOW.as_secs()\n 1293│                );\n 1294│            } else {\n 1295│                let _ = writeln!(\n     │...\n 1299│                     {endpoint_id}` again once the node/daemon is back]\",\n*1300│                    RECONNECT_WINDOW.as_secs()\n 1301│                );\n 1302│            }\n 1303│            Ok(())\n     │...\n 1345│/// F-1 (REQ-PUBLIC-ERROR-SURFACES): the residual attach-failure line is the one\n*1346│/// rc surface that still leaked transport lingo (\"brain IPC read deadline\n 1347│/// elapsed\") to an operator. Map the known daemon-didn't-answer class to the\n 1348│/// observable situation + next action; every OTHER error passes through\n 1349│/// verbatim (no over-matching — unknown failures keep their detail). Pure, so\n     │...\n 1352│fn public_attach_failure(endpoint_id: &str, e: String) -> String {\n*1353│    if e.contains(\"brain IPC read deadline\") {\n 1354│        format!(\n 1355│            \"the daemon didn't answer in time while attaching to '{endpoint_id}' — \\\n 1356│             it may be busy or mid-restart. Try `spt rc {endpoint_id}` again; if it \\\n     │...\n 1367│/// not just the stream; a fresh OpMinter keeps ADR-0034 `rc` op tracing per\n*1368│/// viewport). [impl->REQ-RC-RECONNECT]\n 1369│struct Established {\n 1370│    brain: Brain,\n 1371│    ops: OpMinter,\n     │...\n 1385│    /// The daemon isn't running — rc is CONNECT-ONLY and refuses to auto-start it\n*1386│    /// (REQ-RC-RECONNECT-TRUTH). On the initial attach this is a loud \"daemon\n 1387│    /// isn't running — start it\" exit; inside the reconnect loop it's just another\n 1388│    /// retry (the operator may bring the daemon back), never a resurrection.\n*1389│    /// [impl->REQ-RC-RECONNECT-TRUTH]\n 1390│    DaemonDown,\n 1391│    /// A setup error (broker connect / dial / attach-open / subscribe).\n 1392│    Error(String),\n     │...\n 1397│/// the A-4b tracing retry) → stream subscribe. Factored from `run_attach_inner`\n*1398│/// so the B-3 reconnect loop can re-drive it verbatim (REQ-RC-RECONNECT).\n 1399│///\n 1400│/// Transport for a LOCAL attach: a broker-minted in-process LOOPBACK connection\n 1401│/// (M12 W1.5, B1). A QUIC self-dial is refused by iroh (\"Connecting to ourself\n     │...\n 1415│/// a hang (D2/D6).\n*1416│// [impl->REQ-RC-1] [impl->REQ-RC-CROSS-NODE-ATTACH] [impl->REQ-RC-RECONNECT]\n 1417│fn establish_attach(endpoint_id: &str, intent: AttachIntent) -> Result<Established, EstablishFail> {\n*1418│    // CONNECT-ONLY (REQ-RC-RECONNECT-TRUTH): rc is an attach pump, NOT a daemon\n 1419│    // launcher. It must NEVER auto-start the broker — the historical\n 1420│    // `ensure_running()` here WMI-resurrected a daemon on every (re)connect,\n 1421│    // which fought the operator's `spt daemon stop` (the \"stop 2-4 times\" bug)\n     │...\n 1424│    // loud (initial) or keeps its bounded retry without spawning (reconnect). The\n*1425│    // broker cold-connect below is already deadline-bounded, so a down daemon\n 1426│    // never hangs.\n*1427│    // [impl->REQ-RC-RECONNECT-TRUTH]\n 1428│    if !spt_daemon::daemon::is_running() {\n 1429│        return Err(EstablishFail::DaemonDown);\n 1430│    }\n*1431│    // PUMP mode: the Split reader-thread carrier bounds every read by a deadline\n 1432│    // (a `Whole` conn can't time out — nonblocking is dead on Windows named\n 1433│    // pipes), so the single-threaded pump interleaves stdin + broker events\n 1434│    // without wedging. The generous io_timeout bounds the setup calls (dial,\n     │...\n 1531│/// Drive an established viewport to its FINAL end: the B-3 reconnect loop\n*1532│/// (REQ-RC-RECONNECT) — each pass is one full viewport (initial resize → pump).\n 1533│/// A severed viewport (broker bounce, remote conn drop — the reconnectable\n 1534│/// class) paints a centered reconnect banner and RE-DRIVES [`establish_attach`]\n 1535│/// inside a bounded window; every other end is FINAL and returns to the\n\n## spt-daemon/src/\n### attach.rs\n 107│/// an ordinary fatal so the serve loop can surface it (a marked truncation),\n*108│/// rather than silent-skipping (a B2 lie) or spinning. Full graceful handling is\n 109│/// DEFERRED to `REQ-HAZARD-CONTROLLER-IRRECOVERABLE-BEHIND`.\n 110│// [impl->REQ-HAZARD-CONTROLLER-GAP-RESUME]\n 111│#[derive(Debug)]\n### broker.rs\n  64│    KIND_ADAPTER_APPLY, KIND_APPLIED,\n *65│    KIND_BROKER_IMAGE, KIND_BROKER_IMAGE_REPLY, KIND_STALL_EVICTS, KIND_STALL_EVICTS_REPLY,\n  66│    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,\n  67│    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_OPENER, KIND_NET_STREAM_OPENER_REPLY, KIND_NET_STREAM_RETIRE, KIND_NET_STREAM_RETIRED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,\n  68│    KIND_NET_STREAM_UNSUBSCRIBE, KIND_NET_STREAM_UNSUBSCRIBED,\n    │...\n 143│/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer\n*144│/// never trips the deadline.\n 145│const CONTROLLER_CHANNEL_DEPTH: usize = 4096;\n 146│\n 147│/// How long a CONTROLLER's channel may stay FULL (no successful handoff since\n    │...\n 170│/// A slow-but-DRAINING brain never trips it — the clock only advances while a write\n*171│/// is genuinely in flight past the deadline (see [`ControllerSink::write_blocked_since`]).\n 172│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 173│const BRAIN_WRITE_DEADLINE: Duration = Duration::from_secs(15);\n 174│\n*175│/// The EFFECTIVE brain-write deadline. Production is [`BRAIN_WRITE_DEADLINE`]; the\n 176│/// suspend-brain int rig shrinks it via `SPT_BRAIN_WRITE_DEADLINE_MS` so the\n 177│/// structural outcome (a wedged controller is stall-evicted → take/reattach frees\n 178│/// the slot) is provable in ~1 s instead of 15 (mirrors the\n    │...\n 181│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*182│fn brain_write_deadline() -> Duration {\n 183│    match std::env::var(\"SPT_BRAIN_WRITE_DEADLINE_MS\") {\n 184│        Ok(ms) => ms\n 185│            .parse::<u64>()\n    │...\n 232│// [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\n*233│fn wake_gate_decision(live: bool, claimed: bool, past_deadline: bool) -> WakeGate {\n 234│    if live {\n 235│        WakeGate::AlreadyLive\n*236│    } else if claimed && !past_deadline {\n 237│        WakeGate::Racing\n 238│    } else {\n 239│        WakeGate::Claim\n    │...\n 242│\n*243│/// Boot grace before an adapter session with no live client chain can read as a\n 244│/// zombie (ADR-0041 decision 6): a just-spawned harness wrapper has not forked\n 245│/// its clients yet. Env-tunable so the int rig is deterministic.\n*246│fn spawn_client_grace() -> Duration {\n 247│    std::env::var(\"SPT_SPAWN_CLIENT_GRACE_MS\")\n 248│        .ok()\n 249│        .and_then(|v| v.parse::<u64>().ok())\n    │...\n 261│///     forks a harness client chain under the wrapper by construction), PAST\n*262│///     the boot grace, whose wrapper has NO live descendants — the deployah\n 263│///     wedge shape: the launcher shell idles on while the harness underneath\n 264│///     it died, so `run` said ALREADY_LIVE, `list` said OFFLINE, and\n 265│///     `shutdown` said NO_EDGE about the same corpse.\n    │...\n 274│    has_live_descendants: bool,\n*275│    past_grace: bool,\n 276│) -> bool {\n 277│    match wrapper_alive {\n 278│        None => false,        // no probeable pid — never guess\n 279│        Some(false) => true,  // dead root, surviving record — always a zombie\n*280│        Some(true) => adapter_labeled && past_grace && !has_live_descendants,\n 281│    }\n 282│}\n 283│\n    │...\n 296│    });\n*297│    let past_grace = Duration::from_millis(spawned_ms_ago) >= spawn_client_grace();\n*298│    zombie_verdict(wrapper_alive, adapter_labeled, has_live_descendants, past_grace)\n 299│}\n 300│\n 301│/// Whether a stall-evict re-stamps the perch INLINE (take/reattach path, already\n    │...\n 316│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n*317│static STALL_EVICT_COUNT: AtomicU64 = AtomicU64::new(0);\n*318│static STALL_EVICT_LAST_MS: AtomicU64 = AtomicU64::new(0);\n 319│\n 320│/// Record one stall-evict in the broker-global tally (best-effort observability).\n 321│// [impl->REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE]\n 322│fn record_stall_evict() {\n*323│    STALL_EVICT_COUNT.fetch_add(1, Ordering::Relaxed);\n*324│    STALL_EVICT_LAST_MS.store(crate::brain::now_ms(), Ordering::Relaxed);\n 325│}\n 326│\n 327│/// The current stall-evict tally `(count_since_boot, last_evict_wall_ms)` —\n    │...\n 332│    (\n*333│        STALL_EVICT_COUNT.load(Ordering::Relaxed),\n*334│        STALL_EVICT_LAST_MS.load(Ordering::Relaxed),\n 335│    )\n 336│}\n 337│\n### conn.rs\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    │...\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    │...\n 229│    /// Render the once-per-conn retirement record — the F-039 token split\n*230│    /// (leg a): `CONN_WRITE_POISONED` is RESERVED for the deadline class\n 231│    /// (`timed_out` latched — the wedge observable blackhole-controller.ps1\n 232│    /// watches); an ORGANIC write failure (peer death / broken pipe on an\n 233│    /// already-dead counterpart, the offline-peer dial churn) emits the\n    │...\n 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    │...\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    │...\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    │...\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 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 {"},"isError":false,"timestamp":1784365505180}}
{"type":"message","id":"881df5f1","parentId":"fc04909a","timestamp":"2026-07-18T09:05:05.184Z","message":{"role":"toolResult","toolCallId":"call_f5orIrgxrs7dXTDNuYBfyhd4|fc_0ce5441d29b9f05d016a5b41c0fc10819b865cb3bc7c0f6b59","toolName":"read","content":[{"type":"text","text":"pub fn serve_attach(\n…\n) -> io::Result<AttachServeOutcome> {\n…\n    loop {\n…\n        match event {\n…\n            BrokerEvent::Subscribed { outcome, .. } => {\n…\n                match outcome {\n…\n                    O::BusyControlled { .. } => {\n…\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_gen(sid, Some(serve_gen)); // release ONLY our generation\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    // RC attach/view streams are ConnectionBound (ADR-0040 decision 6): this\n    // viewport's stream dies with the brain conn that opened it — the opener\n    // broker FINs it on conn loss, so the target's serve sees EOF and frees the\n    // controller instead of pinning it forever (the emphasys C2 raw-close leak).\n    // Inter-brain streams elsewhere stay Durable.\n    // [impl->REQ-STREAM-LEASE-CLASSES]\n    let opened = brain.net_open_stream_classed(\n        conn_id,\n        Some(open_op),\n        crate::msg::StreamLifetime::ConnectionBound,\n    )?;\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        // Attach generation (ADR-0038 Amendment fix 6): wall-ms at viewport\n        // start — a stale replay of this Request can never displace a newer\n        // same-identity controller.\n        // [impl->REQ-SERVE-OWNERSHIP-GENERATION]\n        gen: now_ms(),\n    });\n    brain.net_stream_send(opened.stream_id, &line, None, false)?;\n    Ok(opened.stream_id)\n}\n\n/// Operator-side (CONTROLLER): send a resize record onto an attach stream —\n/// the controller's viewport size, forwarded into the target PTY (REQ-RCVIEW-1,\n/// controller-exclusive: the broker rejects a resize from a viewer's stream).\n/// Sent on attach (initial size) and on every window-change. Unjournaled (a\n/// stale resize is harmless — the next one corrects it).\n// [impl->REQ-RCVIEW-1]\npub fn send_attach_resize(\n    brain: &mut Brain,\n    stream_id: u64,\n    rows: u16,\n    cols: u16,\n) -> io::Result<()> {\n    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\n    brain.net_stream_send(stream_id, &line, None, false)?;\n    Ok(())\n}\n\n/// Operator-side: send one input record (the keystrokes) onto an attach\n/// stream. `wire_op` is the operator-minted durable id the TARGET journals the\n/// PTY write under — exactly-once at the keys, however often the record\n/// crosses the wire. The send itself is unjournaled (see module docs).\n// [impl->REQ-INST-8]\npub fn send_attach_input(\n    brain: &mut Brain,\n    stream_id: u64,\n    bytes: &[u8],\n    wire_op: u64,\n) -> io::Result<()> {\n    let line = ndjson::encode_line(&AttachRecord::Input {\n        data_b64: encode_bytes(bytes),\n        op_id: wire_op,\n    });\n    brain.net_stream_send(stream_id, &line, None, false)?;\n    Ok(())\n}\n"}],"details":{"displayContent":{"text":"pub fn serve_attach(\n…\n) -> io::Result<AttachServeOutcome> {\n…\n    loop {\n…\n        match event {\n…\n            BrokerEvent::Subscribed { outcome, .. } => {\n…\n                match outcome {\n…\n                    O::BusyControlled { .. } => {\n…\n                        wire.net_stream_send(stream_id, &[], None, true)?;\r\n                        return Ok(AttachServeOutcome::Refused);\r\n                    }\r\n                }\r\n            }\r\n            // Viewer letterbox feed: the current PTY size (on attach + resize).\r\n            BrokerEvent::Size { rows, cols, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            // This controller was kicked by a different operator's `--take`.\r\n            BrokerEvent::Displaced { by, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Displaced { by });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                // No dormant edge: the endpoint stays driven by the taker (whose\r\n                // own serve loop already fired the wake).\r\n                return Ok(AttachServeOutcome::Displaced);\r\n            }\r\n\r\n            // This VIEWER overflowed its broker subscription queue and was evicted\r\n            // (it forwards slower than the drain fans out under flood). SKIP TO\r\n            // LIVE — re-subscribe from the current ring floor — instead of tearing\r\n            // the attach down: DISTINCT from a session-exit EOF. Rate-limited\r\n            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained\r\n            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU\r\n            // spin. The next live Output carries a forward seq jump; the operator's\r\n            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.\r\n            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]\r\n            BrokerEvent::ViewerEvicted { session_id: sid }\r\n                if attached && Some(sid) == attached_session =>\r\n            {\r\n                if let Some(prev) = last_resubscribe {\r\n                    let since = prev.elapsed();\r\n                    if since < RESUBSCRIBE_INTERVAL {\r\n                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);\r\n                    }\r\n                }\r\n                brain.attach_skip_to_live(sid, Some(origin_node))?;\r\n                last_resubscribe = Some(Instant::now());\r\n            }\r\n\r\n            // The operator closed its send side: the viewport is over.\r\n            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {\r\n                wire.net_stream_send(stream_id, &[], None, true)?;\r\n                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes\r\n                // info.json, carrying the current driven_by through), THEN release\r\n                // the role over IPC so the broker's driven_by=None is the LAST\r\n                // write — otherwise a concurrent resting RMW restores the stale\r\n                // marker (the clear-race the broker-single-writer model averts).\r\n                if role == Some(ServeRole::Controller) {\r\n                    // The driver detached — the endpoint rests warm (controller-\r\n                    // only; a viewer leaving is wake-neutral).\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                if let Some(sid) = attached_session {\r\n                    let _ = brain.detach_session_gen(sid, Some(serve_gen)); // release ONLY our generation\r\n                }\r\n                return Ok(AttachServeOutcome::Detached);\r\n            }\r\n\r\n            // ── Session → wire: output and exit. ────────────────────────────\r\n            BrokerEvent::Output { seq, bytes, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Output {\r\n                    seq,\r\n                    data_b64: encode_bytes(&bytes),\r\n                });\r\n                // Unjournaled by design (see module docs): `seq` rides inside\r\n                // the record, and the operator's render cursor is the dedup.\r\n                wire.net_stream_send(stream_id, &line, None, false)?;\r\n            }\r\n            BrokerEvent::Exit { code, .. } if attached => {\r\n                let line = ndjson::encode_line(&AttachRecord::Exit { code });\r\n                wire.net_stream_send(stream_id, &line, None, true)?;\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Ok(AttachServeOutcome::Exited(code));\r\n            }\r\n\r\n            BrokerEvent::Error { message } => {\r\n                if role == Some(ServeRole::Controller) {\r\n                    feed_rest(endpoint, crate::resting::RestEvent::Detach);\r\n                }\r\n                return Err(io::Error::other(message));\r\n            }\r\n            // Applied/NetSent acks and unrelated events: not ours to handle.\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n/// Operator-side: open an attach stream on a dialed connection and send the\r\n/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes\r\n/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes\r\n/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the\r\n/// durable id for the exactly-once stream open (safe to block for its ack —\r\n/// no attach traffic can be in flight before the stream exists); the Request\r\n/// send itself is unjournaled (see module docs — a replayed Request re-gates\r\n/// and re-attaches idempotently).\r\n// [impl->REQ-INST-8]\r\npub fn request_attach(\r\n    brain: &mut Brain,\r\n    conn_id: u64,\r\n    session_id: u64,\r\n    from_seq: u64,\r\n    open_op: MintedOp,\r\n    intent: AttachIntent,\r\n) -> io::Result<u64> {\r\n    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\r\n}\r\n\r\n/// Like [`request_attach`], but for the cross-node leg (#4,\r\n/// REQ-RC-CROSS-NODE-ATTACH): the remote operator cannot know the owning node's\r\n/// local session id, so it passes `endpoint_id = Some(endpoint)` (and a\r\n/// placeholder `session_id` of `0` the owning node ignores). The owning node\r\n/// resolves the endpoint to its OWN session id server-side\r\n/// ([`resolve_local_session`]) — authoritative for its own table. `None` here is\r\n/// exactly [`request_attach`]: the local path's wire bytes stay byte-identical.\r\n// [impl->REQ-RC-CROSS-NODE-ATTACH]\r\npub fn request_attach_endpoint(\r\n    brain: &mut Brain,\r\n    conn_id: u64,\r\n    session_id: u64,\r\n    from_seq: u64,\r\n    open_op: MintedOp,\r\n    intent: AttachIntent,\r\n    endpoint_id: Option<&str>,\r\n) -> io::Result<u64> {\r\n    // RC attach/view streams are ConnectionBound (ADR-0040 decision 6): this\r\n    // viewport's stream dies with the brain conn that opened it — the opener\r\n    // broker FINs it on conn loss, so the target's serve sees EOF and frees the\r\n    // controller instead of pinning it forever (the emphasys C2 raw-close leak).\r\n    // Inter-brain streams elsewhere stay Durable.\r\n    // [impl->REQ-STREAM-LEASE-CLASSES]\r\n    let opened = brain.net_open_stream_classed(\r\n        conn_id,\r\n        Some(open_op),\r\n        crate::msg::StreamLifetime::ConnectionBound,\r\n    )?;\r\n    let line = ndjson::encode_line(&AttachRecord::Request {\r\n        session_id,\r\n        from_seq,\r\n        intent,\r\n        endpoint_id: endpoint_id.map(str::to_string),\r\n        // Attach generation (ADR-0038 Amendment fix 6): wall-ms at viewport\r\n        // start — a stale replay of this Request can never displace a newer\r\n        // same-identity controller.\r\n        // [impl->REQ-SERVE-OWNERSHIP-GENERATION]\r\n        gen: now_ms(),\r\n    });\r\n    brain.net_stream_send(opened.stream_id, &line, None, false)?;\r\n    Ok(opened.stream_id)\r\n}\r\n\r\n/// Operator-side (CONTROLLER): send a resize record onto an attach stream —\r\n/// the controller's viewport size, forwarded into the target PTY (REQ-RCVIEW-1,\r\n/// controller-exclusive: the broker rejects a resize from a viewer's stream).\r\n/// Sent on attach (initial size) and on every window-change. Unjournaled (a\r\n/// stale resize is harmless — the next one corrects it).\r\n// [impl->REQ-RCVIEW-1]\r\npub fn send_attach_resize(\r\n    brain: &mut Brain,\r\n    stream_id: u64,\r\n    rows: u16,\r\n    cols: u16,\r\n) -> io::Result<()> {\r\n    let line = ndjson::encode_line(&AttachRecord::Resize { rows, cols });\r\n    brain.net_stream_send(stream_id, &line, None, false)?;\r\n    Ok(())\r\n}\r\n\r\n/// Operator-side: send one input record (the keystrokes) onto an attach\r\n/// stream. `wire_op` is the operator-minted durable id the TARGET journals the\r\n/// PTY write under — exactly-once at the keys, however often the record\r\n/// crosses the wire. The send itself is unjournaled (see module docs).\r\n// [impl->REQ-INST-8]\r\npub fn send_attach_input(\r\n    brain: &mut Brain,\r\n    stream_id: u64,\r\n    bytes: &[u8],\r\n    wire_op: u64,\r\n) -> io::Result<()> {\r\n    let line = ndjson::encode_line(&AttachRecord::Input {\r\n        data_b64: encode_bytes(bytes),\r\n        op_id: wire_op,\r\n    });\r\n    brain.net_stream_send(stream_id, &line, None, false)?;\r\n    Ok(())\r\n}\r\n","startLine":304,"lineNumbers":[304,null,313,null,392,null,430,null,515,null,517,null,535,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\attach.rs"}}},"isError":false,"timestamp":1784365505184}}
{"type":"message","id":"2907ee09","parentId":"881df5f1","timestamp":"2026-07-18T09:05:05.231Z","message":{"role":"toolResult","toolCallId":"call_k3MhmjWJ5bToC5p8L2PRjEos|fc_0ce5441d29b9f05d016a5b41c0fc14819bb2da97fe43b4d805","toolName":"read","content":[{"type":"text","text":"    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n…\n        loop {\n…\n            match env.kind.as_str() {\n…\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 => match self.dispatch_net_stream_open(env, &send) {\n                    // ConnectionBound open → track for the conn-exit sweep\n                    // (a journaled replay returns the SAME id — dedup keeps\n                    // the sweep list exact).\n                    // [impl->REQ-STREAM-LEASE-CLASSES]\n                    Ok(Some(id)) if !my_cb_streams.contains(&id) => my_cb_streams.push(id),\n                    Ok(_) => {}\n                    Err(msg) => send_error(&send, &msg),\n                },\n                KIND_NET_STREAM_SEND => {\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAMS => {\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_RETIRE => {\n                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_NET_STREAM_OPENER => {\n                    // TEST-RIG-ONLY (T7 mixed-image N-1): with\n                    // SPT_TEST_STREAM_OPENER_UNSUPPORTED set this broker\n                    // answers the verb EXACTLY like an N-1 broker that\n                    // predates it — the forward-compat unknown-kind reply\n                    // below, byte-identical (the stable KH-2.3 wire fact the\n                    // dispatcher's fallback gate keys on). Never set in\n                    // production.\n                    if std::env::var(\"SPT_TEST_STREAM_OPENER_UNSUPPORTED\").is_ok() {\n                        send_error(&send, &format!(\"unknown command kind {:?}\", env.kind));\n                    } else if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {\n                        send_error(&send, &msg);\n                    }\n                }\n                KIND_SESSIONS => {\n                    // Clone the row facts out from under the sessions lock, then\n                    // read each log's cursor under the log lock alone — never\n                    // both locks at once (the dispatch_subscribe discipline).\n                    // pid/adapter/age ride along (ADR-0041 decision 6): the ONE\n                    // cycle-verb liveness authority's inputs, additive on the\n                    // reply so the CLI preflight and the shutdown edge consult\n                    // the SAME zombie verdict the spawn gate enforces.\n                    // [impl->REQ-ENDPOINT-CYCLE-HONEST]\n                    type SessRow = (u64, String, Arc<Mutex<OutputLog>>, Option<u32>, String, u64);\n                    let rows: Vec<SessRow> = {\n                        let sessions = recover(&self.sessions);\n                        sessions\n                            .iter()\n                            .map(|(id, h)| {\n                                (\n                                    *id,\n                                    h.endpoint.clone(),\n                                    Arc::clone(&h.log),\n                                    h.session.process_id(),\n                                    h.adapter.clone(),\n                                    h.spawned_at.elapsed().as_millis() as u64,\n                                )\n                            })\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                        // The endpoint's stamp generation AT snapshot time\n                        // (ADR-0041 decision 4) — the off-lock converge below\n                        // validates against it so a reap that lands between\n                        // this snapshot and the write refuses the stale stamp.\n                        stamp_gen: u64,\n                        // ADR-0041 decision 6: the zombie-verdict inputs, carried\n                        // to the reply (additive SessionInfo fields).\n                        pid: Option<u32>,\n                        adapter: String,\n                        spawned_ms_ago: u64,\n                    }\n                    let snaps: Vec<SessSnap> = rows\n                        .into_iter()\n                        .map(|(id, endpoint, log, pid, adapter, spawned_ms_ago)| {\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                            let stamp_gen =\n                                stamp_slot(&endpoint).gen.load(Ordering::Acquire);\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                                stamp_gen,\n                                pid,\n                                adapter,\n                                spawned_ms_ago,\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                    // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\n                    for s in &snaps {\n                        let _ = converge_perch_stamps(\n                            &s.endpoint,\n                            s.controller_by.as_deref(),\n                            s.has_controller,\n                            s.viewers,\n                            s.stamp_gen,\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                                pid: s.pid,\n                                adapter: s.adapter,\n                                spawned_ms_ago: s.spawned_ms_ago,\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                // Explicit seat release (ADR-0040 decision 5): the released\n                // stream also leaves this conn's sweep list, so the exit\n                // sweep does not re-detach a seat that is already gone.\n                // [impl->REQ-SEAT-LIFETIME-BOUNDED]\n                KIND_NET_STREAM_UNSUBSCRIBE => {\n                    match self.dispatch_net_stream_unsubscribe(env, &send) {\n                        Ok(id) => my_stream_subs.retain(|s| *s != id),\n                        Err(msg) => send_error(&send, &msg),\n                    }\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        // Clone the log Arcs out from under the sessions lock FIRST (ADR-0038\n        // Amendment fix 8, KH 7.12 kin): `detach_if` re-stamps info.json\n        // (`viewer_count`/`driven_by`) and emits a lifecycle record — file +\n        // conn I/O that must never run under the shared sessions table lock,\n        // or one slow perch write stalls every spawn/attach/kill dispatch.\n        // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\n        let logs: Vec<Arc<Mutex<OutputLog>>> = {\n            let sessions = recover(&self.sessions);\n            my_subs\n                .iter()\n                .filter_map(|id| sessions.get(id).map(|h| Arc::clone(&h.log)))\n                .collect()\n        };\n        for log in &logs {\n            recover_log(log).detach_if(&send);\n        }\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            // ConnectionBound teardown (ADR-0040 decision 6): rows THIS conn\n            // opened as ConnectionBound die with it. FIN the stream first — the\n            // remote target's read pump sees EOF, `serve_attach` runs\n            // `detach_session` under its own serve generation (fix-6 validated:\n            // a stale opener's late close can never evict a newer controller),\n            // and the controller/viewer stamps clear — then terminal-retire the\n            // local row (eligibility + physical teardown, the W1 machinery).\n            // This is exactly the emphasys C2 hole: an rc viewport brain dying\n            // WITHOUT FIN left its attach stream restart-durable forever, the\n            // target never saw EOF, and CONTROLLED survived even a full broker\n            // restart. Durable rows (inter-brain) are untouched — brain\n            // disconnect must NEVER globally retire (brain-swap continuity).\n            // [impl->REQ-STREAM-LEASE-CLASSES]\n            for id in &my_cb_streams {\n                let _ = host.send_stream(*id, &[], true);\n                let _ = host.retire_stream_terminal(*id);\n                eprintln!(\"STREAM_CONNBOUND_RETIRE:{id}: opener conn exited — FIN + terminal retire\");\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[Showing lines 3234-3513 of 6771. Use :3514 to continue]"}],"details":{"truncation":{"content":"                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\r\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\r\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\r\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\r\n                KIND_NET_DIAL => {\r\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_DIAL_LOOPBACK => {\r\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-PUMP-PEER-ISOLATION]\r\n                KIND_NET_DIAL_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_JOIN => {\r\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_MEET => {\r\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_CODE_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPEN => match self.dispatch_net_stream_open(env, &send) {\r\n                    // ConnectionBound open → track for the conn-exit sweep\r\n                    // (a journaled replay returns the SAME id — dedup keeps\r\n                    // the sweep list exact).\r\n                    // [impl->REQ-STREAM-LEASE-CLASSES]\r\n                    Ok(Some(id)) if !my_cb_streams.contains(&id) => my_cb_streams.push(id),\r\n                    Ok(_) => {}\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_NET_STREAM_SEND => {\r\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAMS => {\r\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_RETIRE => {\r\n                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPENER => {\r\n                    // TEST-RIG-ONLY (T7 mixed-image N-1): with\r\n                    // SPT_TEST_STREAM_OPENER_UNSUPPORTED set this broker\r\n                    // answers the verb EXACTLY like an N-1 broker that\r\n                    // predates it — the forward-compat unknown-kind reply\r\n                    // below, byte-identical (the stable KH-2.3 wire fact the\r\n                    // dispatcher's fallback gate keys on). Never set in\r\n                    // production.\r\n                    if std::env::var(\"SPT_TEST_STREAM_OPENER_UNSUPPORTED\").is_ok() {\r\n                        send_error(&send, &format!(\"unknown command kind {:?}\", env.kind));\r\n                    } else if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_SESSIONS => {\r\n                    // Clone the row facts out from under the sessions lock, then\r\n                    // read each log's cursor under the log lock alone — never\r\n                    // both locks at once (the dispatch_subscribe discipline).\r\n                    // pid/adapter/age ride along (ADR-0041 decision 6): the ONE\r\n                    // cycle-verb liveness authority's inputs, additive on the\r\n                    // reply so the CLI preflight and the shutdown edge consult\r\n                    // the SAME zombie verdict the spawn gate enforces.\r\n                    // [impl->REQ-ENDPOINT-CYCLE-HONEST]\r\n                    type SessRow = (u64, String, Arc<Mutex<OutputLog>>, Option<u32>, String, u64);\r\n                    let rows: Vec<SessRow> = {\r\n                        let sessions = recover(&self.sessions);\r\n                        sessions\r\n                            .iter()\r\n                            .map(|(id, h)| {\r\n                                (\r\n                                    *id,\r\n                                    h.endpoint.clone(),\r\n                                    Arc::clone(&h.log),\r\n                                    h.session.process_id(),\r\n                                    h.adapter.clone(),\r\n                                    h.spawned_at.elapsed().as_millis() as u64,\r\n                                )\r\n                            })\r\n                            .collect()\r\n                    };\r\n                    // Snapshot each session's truth under its OWN log lock (cursor +\r\n                    // controller identity + the control/viewer stamp truth), doing NO\r\n                    // file I/O under the lock (the lock-across-effect discipline, KH\r\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\r\n                    // the lock.\r\n                    struct SessSnap {\r\n                        id: u64,\r\n                        endpoint: String,\r\n                        resume_seq: u64,\r\n                        controller_by: Option<String>,\r\n                        has_controller: bool,\r\n                        viewers: u32,\r\n                        // The endpoint's stamp generation AT snapshot time\r\n                        // (ADR-0041 decision 4) — the off-lock converge below\r\n                        // validates against it so a reap that lands between\r\n                        // this snapshot and the write refuses the stale stamp.\r\n                        stamp_gen: u64,\r\n                        // ADR-0041 decision 6: the zombie-verdict inputs, carried\r\n                        // to the reply (additive SessionInfo fields).\r\n                        pid: Option<u32>,\r\n                        adapter: String,\r\n                        spawned_ms_ago: u64,\r\n                    }\r\n                    let snaps: Vec<SessSnap> = rows\r\n                        .into_iter()\r\n                        .map(|(id, endpoint, log, pid, adapter, spawned_ms_ago)| {\r\n                            let mut log = recover_log(&log);\r\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\r\n                            // truth HONEST before reading it — reap a dead-writer\r\n                            // (severed) controller so both the reply's controller_by\r\n                            // AND the off-lock converge_perch_stamps below see the\r\n                            // cleared state and the stale info.json stamp clears.\r\n                            let _ = log.reap_dead_controller();\r\n                            let stamp_gen =\r\n                                stamp_slot(&endpoint).gen.load(Ordering::Acquire);\r\n                            SessSnap {\r\n                                id,\r\n                                endpoint,\r\n                                resume_seq: log.delivered_through(),\r\n                                controller_by: log.controller_by().map(str::to_string),\r\n                                has_controller: log.has_controller(),\r\n                                viewers: log.live_viewer_count(),\r\n                                stamp_gen,\r\n                                pid,\r\n                                adapter,\r\n                                spawned_ms_ago,\r\n                            }\r\n                        })\r\n                        .collect();\r\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\r\n                    // each live session's control/viewer stamps to broker truth,\r\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\r\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\r\n                    // pre-bind); converges within one reconcile-poll window after bind.\r\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n                    // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\n                    for s in &snaps {\r\n                        let _ = converge_perch_stamps(\r\n                            &s.endpoint,\r\n                            s.controller_by.as_deref(),\r\n                            s.has_controller,\r\n                            s.viewers,\r\n                            s.stamp_gen,\r\n                        );\r\n                    }\r\n                    let reply = SessionsReply {\r\n                        sessions: snaps\r\n                            .into_iter()\r\n                            .map(|s| SessionInfo {\r\n                                session_id: s.id,\r\n                                endpoint: s.endpoint,\r\n                                resume_seq: s.resume_seq,\r\n                                controller_by: s.controller_by,\r\n                                pid: s.pid,\r\n                                adapter: s.adapter,\r\n                                spawned_ms_ago: s.spawned_ms_ago,\r\n                            })\r\n                            .collect(),\r\n                    };\r\n                    let frame = Envelope::new(\r\n                        KIND_SESSIONS_REPLY,\r\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\r\n                    );\r\n                    send_frame(&send, &frame);\r\n                }\r\n                KIND_ENDPOINT_INPUT => {\r\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\r\n                    Ok(id) => my_stream_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                // Explicit seat release (ADR-0040 decision 5): the released\r\n                // stream also leaves this conn's sweep list, so the exit\r\n                // sweep does not re-detach a seat that is already gone.\r\n                // [impl->REQ-SEAT-LIFETIME-BOUNDED]\r\n                KIND_NET_STREAM_UNSUBSCRIBE => {\r\n                    match self.dispatch_net_stream_unsubscribe(env, &send) {\r\n                        Ok(id) => my_stream_subs.retain(|s| *s != id),\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_NET_PRESENCE_SUBSCRIBE => {\r\n                    match self.dispatch_net_presence_subscribe(env, &send) {\r\n                        Ok(()) => my_presence_sub = true,\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\r\n                // An unknown kind is tolerated (forward-compat): report, don't die.\r\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\r\n            }\r\n        }\r\n\r\n        // Brain gone: detach as subscriber but leave the sessions running.\r\n        // Clone the log Arcs out from under the sessions lock FIRST (ADR-0038\r\n        // Amendment fix 8, KH 7.12 kin): `detach_if` re-stamps info.json\r\n        // (`viewer_count`/`driven_by`) and emits a lifecycle record — file +\r\n        // conn I/O that must never run under the shared sessions table lock,\r\n        // or one slow perch write stalls every spawn/attach/kill dispatch.\r\n        // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\r\n        let logs: Vec<Arc<Mutex<OutputLog>>> = {\r\n            let sessions = recover(&self.sessions);\r\n            my_subs\r\n                .iter()\r\n                .filter_map(|id| sessions.get(id).map(|h| Arc::clone(&h.log)))\r\n                .collect()\r\n        };\r\n        for log in &logs {\r\n            recover_log(log).detach_if(&send);\r\n        }\r\n        // Same for net streams: the streams + their rings persist (D4b).\r\n        if let Some(host) = self.net.get() {\r\n            for id in &my_stream_subs {\r\n                host.detach_stream_sub(*id, &send);\r\n            }\r\n            // ConnectionBound teardown (ADR-0040 decision 6): rows THIS conn\r\n            // opened as ConnectionBound die with it. FIN the stream first — the\r\n            // remote target's read pump sees EOF, `serve_attach` runs\r\n            // `detach_session` under its own serve generation (fix-6 validated:\r\n            // a stale opener's late close can never evict a newer controller),\r\n            // and the controller/viewer stamps clear — then terminal-retire the\r\n            // local row (eligibility + physical teardown, the W1 machinery).\r\n            // This is exactly the emphasys C2 hole: an rc viewport brain dying\r\n            // WITHOUT FIN left its attach stream restart-durable forever, the\r\n            // target never saw EOF, and CONTROLLED survived even a full broker\r\n            // restart. Durable rows (inter-brain) are untouched — brain\r\n            // disconnect must NEVER globally retire (brain-swap continuity).\r\n            // [impl->REQ-STREAM-LEASE-CLASSES]\r\n            for id in &my_cb_streams {\r\n                let _ = host.send_stream(*id, &[], true);\r\n                let _ = host.retire_stream_terminal(*id);\r\n                eprintln!(\"STREAM_CONNBOUND_RETIRE:{id}: opener conn exited — FIN + terminal retire\");\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r","truncated":true,"truncatedBy":"lines","totalLines":3538,"totalBytes":14988,"outputLines":280,"outputBytes":14988,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n…\n        loop {\n…\n            match env.kind.as_str() {\n…\n                KIND_RESIZE => {\n                    if let Err(msg) = self.dispatch_resize(env, &send) {\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_KILL => {\r\n                    if let Err(msg) = self.dispatch_kill(env) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-ADAPTER-LIVE-UPDATE]\r\n                KIND_ADAPTER_APPLY => {\r\n                    if let Err(msg) = self.dispatch_adapter_apply(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STATUS => self.dispatch_net_status(&send),\r\n                // [impl->REQ-UPDATE-RUNNING-IMAGE-SURFACE]\r\n                KIND_BROKER_IMAGE => self.dispatch_broker_image(&send),\r\n                KIND_STALL_EVICTS => self.dispatch_stall_evicts(&send),\r\n                KIND_NET_DIAL => {\r\n                    if let Err(msg) = self.dispatch_net_dial(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_DIAL_LOOPBACK => {\r\n                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                // [impl->REQ-PUMP-PEER-ISOLATION]\r\n                KIND_NET_DIAL_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_net_dial_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_JOIN => {\r\n                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_MEET => {\r\n                    if let Err(msg) = self.dispatch_pair_meet(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_PAIR_CODE_SUBMIT => {\r\n                    if let Err(msg) = self.dispatch_pair_code_submit(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPEN => match self.dispatch_net_stream_open(env, &send) {\r\n                    // ConnectionBound open → track for the conn-exit sweep\r\n                    // (a journaled replay returns the SAME id — dedup keeps\r\n                    // the sweep list exact).\r\n                    // [impl->REQ-STREAM-LEASE-CLASSES]\r\n                    Ok(Some(id)) if !my_cb_streams.contains(&id) => my_cb_streams.push(id),\r\n                    Ok(_) => {}\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                KIND_NET_STREAM_SEND => {\r\n                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAMS => {\r\n                    if let Err(msg) = self.dispatch_net_streams(&send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_RETIRE => {\r\n                    if let Err(msg) = self.dispatch_net_stream_retire(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_OPENER => {\r\n                    // TEST-RIG-ONLY (T7 mixed-image N-1): with\r\n                    // SPT_TEST_STREAM_OPENER_UNSUPPORTED set this broker\r\n                    // answers the verb EXACTLY like an N-1 broker that\r\n                    // predates it — the forward-compat unknown-kind reply\r\n                    // below, byte-identical (the stable KH-2.3 wire fact the\r\n                    // dispatcher's fallback gate keys on). Never set in\r\n                    // production.\r\n                    if std::env::var(\"SPT_TEST_STREAM_OPENER_UNSUPPORTED\").is_ok() {\r\n                        send_error(&send, &format!(\"unknown command kind {:?}\", env.kind));\r\n                    } else if let Err(msg) = self.dispatch_net_stream_opener(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_SESSIONS => {\r\n                    // Clone the row facts out from under the sessions lock, then\r\n                    // read each log's cursor under the log lock alone — never\r\n                    // both locks at once (the dispatch_subscribe discipline).\r\n                    // pid/adapter/age ride along (ADR-0041 decision 6): the ONE\r\n                    // cycle-verb liveness authority's inputs, additive on the\r\n                    // reply so the CLI preflight and the shutdown edge consult\r\n                    // the SAME zombie verdict the spawn gate enforces.\r\n                    // [impl->REQ-ENDPOINT-CYCLE-HONEST]\r\n                    type SessRow = (u64, String, Arc<Mutex<OutputLog>>, Option<u32>, String, u64);\r\n                    let rows: Vec<SessRow> = {\r\n                        let sessions = recover(&self.sessions);\r\n                        sessions\r\n                            .iter()\r\n                            .map(|(id, h)| {\r\n                                (\r\n                                    *id,\r\n                                    h.endpoint.clone(),\r\n                                    Arc::clone(&h.log),\r\n                                    h.session.process_id(),\r\n                                    h.adapter.clone(),\r\n                                    h.spawned_at.elapsed().as_millis() as u64,\r\n                                )\r\n                            })\r\n                            .collect()\r\n                    };\r\n                    // Snapshot each session's truth under its OWN log lock (cursor +\r\n                    // controller identity + the control/viewer stamp truth), doing NO\r\n                    // file I/O under the lock (the lock-across-effect discipline, KH\r\n                    // 7.12/5.16). The convergence re-assert + the reply then run OFF\r\n                    // the lock.\r\n                    struct SessSnap {\r\n                        id: u64,\r\n                        endpoint: String,\r\n                        resume_seq: u64,\r\n                        controller_by: Option<String>,\r\n                        has_controller: bool,\r\n                        viewers: u32,\r\n                        // The endpoint's stamp generation AT snapshot time\r\n                        // (ADR-0041 decision 4) — the off-lock converge below\r\n                        // validates against it so a reap that lands between\r\n                        // this snapshot and the write refuses the stale stamp.\r\n                        stamp_gen: u64,\r\n                        // ADR-0041 decision 6: the zombie-verdict inputs, carried\r\n                        // to the reply (additive SessionInfo fields).\r\n                        pid: Option<u32>,\r\n                        adapter: String,\r\n                        spawned_ms_ago: u64,\r\n                    }\r\n                    let snaps: Vec<SessSnap> = rows\r\n                        .into_iter()\r\n                        .map(|(id, endpoint, log, pid, adapter, spawned_ms_ago)| {\r\n                            let mut log = recover_log(&log);\r\n                            // B-2 (REQ-CONTROLLER-LIVENESS-REAP): make the controller\r\n                            // truth HONEST before reading it — reap a dead-writer\r\n                            // (severed) controller so both the reply's controller_by\r\n                            // AND the off-lock converge_perch_stamps below see the\r\n                            // cleared state and the stale info.json stamp clears.\r\n                            let _ = log.reap_dead_controller();\r\n                            let stamp_gen =\r\n                                stamp_slot(&endpoint).gen.load(Ordering::Acquire);\r\n                            SessSnap {\r\n                                id,\r\n                                endpoint,\r\n                                resume_seq: log.delivered_through(),\r\n                                controller_by: log.controller_by().map(str::to_string),\r\n                                has_controller: log.has_controller(),\r\n                                viewers: log.live_viewer_count(),\r\n                                stamp_gen,\r\n                                pid,\r\n                                adapter,\r\n                                spawned_ms_ago,\r\n                            }\r\n                        })\r\n                        .collect();\r\n                    // STAMP CONVERGENCE (REQ-HAZARD-CONTROL-STAMP-CONVERGENCE): re-assert\r\n                    // each live session's control/viewer stamps to broker truth,\r\n                    // divergence-gated + OFF the log lock. Closes the stamp-before-bind\r\n                    // loss for a picker-created endpoint (its spawn stamp was swallowed\r\n                    // pre-bind); converges within one reconcile-poll window after bind.\r\n                    // [impl->REQ-HAZARD-CONTROL-STAMP-CONVERGENCE]\r\n                    // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\n                    for s in &snaps {\r\n                        let _ = converge_perch_stamps(\r\n                            &s.endpoint,\r\n                            s.controller_by.as_deref(),\r\n                            s.has_controller,\r\n                            s.viewers,\r\n                            s.stamp_gen,\r\n                        );\r\n                    }\r\n                    let reply = SessionsReply {\r\n                        sessions: snaps\r\n                            .into_iter()\r\n                            .map(|s| SessionInfo {\r\n                                session_id: s.id,\r\n                                endpoint: s.endpoint,\r\n                                resume_seq: s.resume_seq,\r\n                                controller_by: s.controller_by,\r\n                                pid: s.pid,\r\n                                adapter: s.adapter,\r\n                                spawned_ms_ago: s.spawned_ms_ago,\r\n                            })\r\n                            .collect(),\r\n                    };\r\n                    let frame = Envelope::new(\r\n                        KIND_SESSIONS_REPLY,\r\n                        serde_json::to_value(reply).expect(\"SessionsReply serializes\"),\r\n                    );\r\n                    send_frame(&send, &frame);\r\n                }\r\n                KIND_ENDPOINT_INPUT => {\r\n                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {\r\n                        send_error(&send, &msg);\r\n                    }\r\n                }\r\n                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {\r\n                    Ok(id) => my_stream_subs.push(id),\r\n                    Err(msg) => send_error(&send, &msg),\r\n                },\r\n                // Explicit seat release (ADR-0040 decision 5): the released\r\n                // stream also leaves this conn's sweep list, so the exit\r\n                // sweep does not re-detach a seat that is already gone.\r\n                // [impl->REQ-SEAT-LIFETIME-BOUNDED]\r\n                KIND_NET_STREAM_UNSUBSCRIBE => {\r\n                    match self.dispatch_net_stream_unsubscribe(env, &send) {\r\n                        Ok(id) => my_stream_subs.retain(|s| *s != id),\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_NET_PRESENCE_SUBSCRIBE => {\r\n                    match self.dispatch_net_presence_subscribe(env, &send) {\r\n                        Ok(()) => my_presence_sub = true,\r\n                        Err(msg) => send_error(&send, &msg),\r\n                    }\r\n                }\r\n                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),\r\n                // An unknown kind is tolerated (forward-compat): report, don't die.\r\n                other => send_error(&send, &format!(\"unknown command kind {other:?}\")),\r\n            }\r\n        }\r\n\r\n        // Brain gone: detach as subscriber but leave the sessions running.\r\n        // Clone the log Arcs out from under the sessions lock FIRST (ADR-0038\r\n        // Amendment fix 8, KH 7.12 kin): `detach_if` re-stamps info.json\r\n        // (`viewer_count`/`driven_by`) and emits a lifecycle record — file +\r\n        // conn I/O that must never run under the shared sessions table lock,\r\n        // or one slow perch write stalls every spawn/attach/kill dispatch.\r\n        // [impl->REQ-DISPATCH-HYGIENE-TELEMETRY]\r\n        let logs: Vec<Arc<Mutex<OutputLog>>> = {\r\n            let sessions = recover(&self.sessions);\r\n            my_subs\r\n                .iter()\r\n                .filter_map(|id| sessions.get(id).map(|h| Arc::clone(&h.log)))\r\n                .collect()\r\n        };\r\n        for log in &logs {\r\n            recover_log(log).detach_if(&send);\r\n        }\r\n        // Same for net streams: the streams + their rings persist (D4b).\r\n        if let Some(host) = self.net.get() {\r\n            for id in &my_stream_subs {\r\n                host.detach_stream_sub(*id, &send);\r\n            }\r\n            // ConnectionBound teardown (ADR-0040 decision 6): rows THIS conn\r\n            // opened as ConnectionBound die with it. FIN the stream first — the\r\n            // remote target's read pump sees EOF, `serve_attach` runs\r\n            // `detach_session` under its own serve generation (fix-6 validated:\r\n            // a stale opener's late close can never evict a newer controller),\r\n            // and the controller/viewer stamps clear — then terminal-retire the\r\n            // local row (eligibility + physical teardown, the W1 machinery).\r\n            // This is exactly the emphasys C2 hole: an rc viewport brain dying\r\n            // WITHOUT FIN left its attach stream restart-durable forever, the\r\n            // target never saw EOF, and CONTROLLED survived even a full broker\r\n            // restart. Durable rows (inter-brain) are untouched — brain\r\n            // disconnect must NEVER globally retire (brain-swap continuity).\r\n            // [impl->REQ-STREAM-LEASE-CLASSES]\r\n            for id in &my_cb_streams {\r\n                let _ = host.send_stream(*id, &[], true);\r\n                let _ = host.retire_stream_terminal(*id);\r\n                eprintln!(\"STREAM_CONNBOUND_RETIRE:{id}: opener conn exited — FIN + terminal retire\");\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r","startLine":3153,"lineNumbers":[3153,null,3188,null,3194,null,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264,3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,3278,3279,3280,3281,3282,3283,3284,3285,3286,3287,3288,3289,3290,3291,3292,3293,3294,3295,3296,3297,3298,3299,3300,3301,3302,3303,3304,3305,3306,3307,3308,3309,3310,3311,3312,3313,3314,3315,3316,3317,3318,3319,3320,3321,3322,3323,3324,3325,3326,3327,3328,3329,3330,3331,3332,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342,3343,3344,3345,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3387,3388,3389,3390,3391,3392,3393,3394,3395,3396,3397,3398,3399,3400,3401,3402,3403,3404,3405,3406,3407,3408,3409,3410,3411,3412,3413,3414,3415,3416,3417,3418,3419,3420,3421,3422,3423,3424,3425,3426,3427,3428,3429,3430,3431,3432,3433,3434,3435,3436,3437,3438,3439,3440,3441,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3479,3480,3481,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500,3501,3502,3503,3504,3505,3506,3507,3508,3509,3510,3511,3512,3513]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6771,"totalBytes":14988,"outputLines":280,"outputBytes":14988,"shownRange":{"start":3234,"end":3513},"nextOffset":3514}}},"isError":false,"timestamp":1784365505231}}
{"type":"message","id":"c7b47184","parentId":"2907ee09","timestamp":"2026-07-18T09:05:20.637Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Identifying terminal cleanup omissions**\n\n**Inspecting terminal rendering and whitespace handling**"},{"type":"toolCall","id":"call_jBbFhEiGQOZHpGpizFH1eEFd|fc_0ce5441d29b9f05d016a5b41d05ff4819bb2b293f882a3a969","name":"read","arguments":{"i":"Reading PTY stream contract","path":"crates/spt-term/src/stream.rs","selector":"1-280"}},{"type":"toolCall","id":"call_IlWjgHNMKThATSDQvls4e1Bp|fc_0ce5441d29b9f05d016a5b41d06004819baf52732eea6d8a6b","name":"read","arguments":{"i":"Reading PTY surface layer","path":"crates/spt-term/src/surface.rs","selector":"1-320"}},{"type":"toolCall","id":"call_MXrX6rulaYS3WBT720C07POH|fc_0ce5441d29b9f05d016a5b41d0600c819ba216b10e7ef210a7","name":"read","arguments":{"i":"Reading PTY session lifecycle","path":"crates/spt-term/src/pty.rs","selector":"1-360"}},{"type":"toolCall","id":"call_GtkRDPpCpWeMGpXiHjTMayZn|fc_0ce5441d29b9f05d016a5b41d06010819bb27a59ac4cebe57a","name":"read","arguments":{"i":"Reading terminal projection logic","path":"crates/spt-term/src/projection.rs","selector":"1-320"}},{"type":"toolCall","id":"call_JNuL2mkAieTgljwpbpZVyqJ6|fc_0ce5441d29b9f05d016a5b41d06018819ba9a5910aacf6eaf6","name":"grep","arguments":{"i":"Finding terminal output filters","pattern":"trim|filter|diff|viewport|write_all|read|Drain|OutputStream|blank|space|clear|erase|Drop","path":"crates/spt-term/src; crates/spt-term/tests","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_mJRWSlg5c3tpbN7jH47zMHjJ|fc_0ce5441d29b9f05d016a5b41d06020819b97bb8f36893dab02","name":"grep","arguments":{"i":"Finding screen repaint tests","pattern":"repaint|blank|space|erase|clear|row|trailing","path":"crates/spt-daemon/src/broker.rs; crates/spt-term/src/screen.rs; crates/spt-daemon/tests; crates/spt/tests","selector":"6000-6500","case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":29289,"output":637,"cacheRead":118272,"cacheWrite":0,"totalTokens":148198,"reasoningTokens":307,"cost":{"input":0.14644500000000002,"output":0.019110000000000002,"cacheRead":0.059135999999999994,"cacheWrite":0,"total":0.224691}},"stopReason":"toolUse","timestamp":1784365505284,"responseId":"resp_0ce5441d29b9f05d016a5b41c18554819b89ea8117402d5b10","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b41c30af0819bb7dc5c906ecc5d4e","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0HQBdUPxUjAjchNujwrD4QE52gRQVYGg2W2ZcKZDrvcdGrj4zKbzFqw3KVrS3XHxVs3uZvVqK_ke5PqwLfgoORB7_3MgVo0Ph7VXjpJNCpUs9Mrlie9N1tcJCCuNzE6OuetTWTMbRTR5uG9WLvPN4TlKZQsWiRZrodgQf1C838S6fx48LeHNAhlz9cUl00uOWwnvi2n4FmSQTH_ek308_SVV9sBqmLTU19kB7KpvzDQlqy3pOPsGoA9oo_Jno6p3Aeng9ASSUqq1Er9JT93uoFeUuu-7mqM-PqGX2vEE6zfBXZvtuc5lcJ9s1Gjjhf3ini9o9o5YMhxFnsLpQWslvq26BfyeYaMSB9M5tUWuVe9ZSyyKfDE4Ex6FVTgH_capNYyStPpfbyPJWpEAS1Mneusol1ZhtITvpQnRSMwoEV-1j6vPO0xlFH3FWDjwJesDlYpBSnvUE6xPPWQMGeFUbmiuKpBasmT8-wevJAdrsvpsIn_Ece7SYayjRhQreQNmSMyF6AReT6d1naBO2WaGYQVL1EXWL1trptPxJOkk0qRSltaEfpg4cQw_mQR6jGcaBkFcQk6w9nPcU3VpiKM00WOBO6soD0yfX72I8EJF8EMn5njpweGBM44Hi-eOFO5s0nDnGYbjpI9ysGE_ZTlj0IfDDBDQiov4wtjTkbawGZgQB53eqg5A4FVrnhqNLxUezqsy38kZ9QoEqPAf-PK81mzAfnMbey8X5lM-iBtTffH6HZsZRiIFjGFPEdwXWUpJymFAI-4aRG6XTbT5aM54X6S1Z8cR-s0XFFwEWMIOrkR179QmlqjNWzWld70hMDD8TBXpx7h8OMrLZiHGAZ5JkHyGhFa8hztQZkPKY4r6PKy_znsuOTMVtsuQjsQEDFr8xDkftGZrNtlhDjhYiqUG9xvNaGYaOwQNhC4VahYehvHuloyFMN73HJQ-oPFtzV9n1ektby3MAsA_O1ZEbUBWM4cA70t57n3P9Yni8qh4ki0kn7hZ4b8FsOWWxV9ngCcZ8pGVD3TJxS1QN6SWopqOKXyeoimN0X0FSo3IkCOnsUE480jr5N0Gf-o3lNmDXnw-SGVAsUGjIuLEa4kO_KMTazB0gW5ZB1Ga2YT-eHaag1-ad5nhCdioQT3nKPzIj0HD47OvupgNyY5q7-A-cPPuCx8y7kWralvfN0yFgYiwU5_rFZD_hRrwi1RF10U78dSI68TV76mZcetH2n-ryj-Uff5TuEVf7m-gJ74jRUkQL_YJIrnyVgsx65Oc_LMZE4ICGoDr1cqqsMCqwndbG1_UbA1pzHU__u78UvVQHMvbPo8ae1-7NT24pPMwdKab9Z_5IFtqxI1gxDamN9RV03oTGZIxphxCTgGrvzfPiywtxY45Z1XyQesccCMefKW5DrJckYNYJZPGkYKB769s2S1HKfuSsycIcPG88J4DruxCoTS5QK7p4ilLUidbZZzmlyv32x-gLGhXYOq8T7-EqXTe8dMUJuaQ_9G4x_cijzPlLUb8CtAvGC56ebLkdwiWxr2wquWddDVFFhxp3ieBd1orFzT2v67tZ-5sGkLHKSnMTkDrnBwe7lswHcIE2dIRuSCaEJJUIqfFpBUfxUa4wx0Pz4WZpOQeS2nGgOrJZkVJIQdf_YKuuWgbx74epD--zTyNrY9OTV75ufV7iDJ8u3cy5HYQPOo-j6Xm7UAFRbEoaqZA6x9dxKB2K3ICd3YX_-RImX-ygXgF_UywtXmgSwdOtvwUbuoR54q6DGGboNtFHBM39YP5Vn83lZ_66QJTdNGbNPEQ37ucuh2NHo-q-61yePqJaNsve8_ytmXRXcCfTjOu7bHEVjfaOfneHWiei3TuYdBGyhIOrsQbAcA06BtAMeLczcfCe4woj1oJ-Y6XZVwfdhmAcOEOgHIgETaGOyWvQUmnLnDyuIgBNwgEnp8yoW-D5mFmHyHBb-wiU8IuYifMhNC3HgPgR5HloJvyZLQNGSBWYUQ3-qAMgLr9_QUC6wPQA_OSfXWZdmyuzJo9E2tGMjzS3bLnAbdH3T-YEHdGjVqiyw8PfHrTbaZmpDJLBiNOg5UsfGuXSn8bjl46aTPLFGcGUFXwl-gcXXz5OoU4sdfcARwZWSniK7kZcqYJ3h1Anwrs61pVMMjYfWu3dlpKsakAS_futgFPS1CokyqOxhv31HbpQ40H6I1B6sWD2MCq_Z9P5lDWa0bI-yF12QmQrzlRH3y8mkDRIq3_GZGZ5A-rvTbvTIkyyqoO4rBNagXCZTDY9kUNpQqKTgDzZFioCsNE0jDlQ1KlqvX4IaI1qk16Mr6Ici1StkVnupXdJODXA_2hoTdlV-kgjH4vXKvviPIhveZitDNgoMYC0XaZ1kDpAfa1jen0VAFnJ6wnRXDC_uXN65NMV-EJ8mOSv5IE1AFbC5x_IY0PWXLiGb-5iO_cf746Gl6gqDnqO9a8tqIUnokjwzYkOKSzS1gBwPmRiKL5hplBPGMEVFrOM3QeolLMmlglEahWXR8VzImvPbTR4UqRAyYvxni6wovEui08GFoBlne46_l_FdwbIn3XCAGClxJPQ7Y6Gtxy9AOHaIScVjZOW_nHCCqD1iFaMukI1WH2MEHQKnbdVJslU2dUi1Z_VeZoz-__LpNq4WclIPWyl7hGcA41XS5ayG8ac-HW1WvcTDxR8W3OTTfLwgNQ_Dipq3Dmp7nDK-vmabeYZ4Fq-UMlUZCMs8ROA4gICi8fG50RODj6fGFy2kbhezOB9iC_fR4IeHlo2vn1aOqzPheWj8e4TGUP60XwTxrUEhdZ9uSzGDSqZX-7RxsgyQExRx_o6_HqjNhJmaEJb4ASrPrGD4OnYiDoKQZT8MDFS8gSKWvB8jSx5YYPU6Up8BvyMLICLXv0dN7PQC-YzDUUVm4oqbwDiGo1D9Kj0PlF4-GlSg=","summary":[{"type":"summary_text","text":"**Identifying terminal cleanup omissions**"},{"type":"summary_text","text":"**Inspecting terminal rendering and whitespace handling**"}]},{"id":"fc_0ce5441d29b9f05d016a5b41d05ff4819bb2b293f882a3a969","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading PTY stream contract\",\"path\":\"crates/spt-term/src/stream.rs\",\"selector\":\"1-280\"}","call_id":"call_jBbFhEiGQOZHpGpizFH1eEFd","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41d06004819baf52732eea6d8a6b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading PTY surface layer\",\"path\":\"crates/spt-term/src/surface.rs\",\"selector\":\"1-320\"}","call_id":"call_IlWjgHNMKThATSDQvls4e1Bp","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41d0600c819ba216b10e7ef210a7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading PTY session lifecycle\",\"path\":\"crates/spt-term/src/pty.rs\",\"selector\":\"1-360\"}","call_id":"call_MXrX6rulaYS3WBT720C07POH","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41d06010819bb27a59ac4cebe57a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading terminal projection logic\",\"path\":\"crates/spt-term/src/projection.rs\",\"selector\":\"1-320\"}","call_id":"call_GtkRDPpCpWeMGpXiHjTMayZn","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41d06018819ba9a5910aacf6eaf6","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding terminal output filters\",\"pattern\":\"trim|filter|diff|viewport|write_all|read|Drain|OutputStream|blank|space|clear|erase|Drop\",\"path\":\"crates/spt-term/src; crates/spt-term/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_JNuL2mkAieTgljwpbpZVyqJ6","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41d06020819b97bb8f36893dab02","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding screen repaint tests\",\"pattern\":\"repaint|blank|space|erase|clear|row|trailing\",\"path\":\"crates/spt-daemon/src/broker.rs; crates/spt-term/src/screen.rs; crates/spt-daemon/tests; crates/spt/tests\",\"selector\":\"6000-6500\",\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_mJRWSlg5c3tpbN7jH47zMHjJ","name":"grep"}]},"duration":15352.209699988365,"ttft":1754.3532999753952,"contextSnapshot":{"promptTokens":147561,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jBbFhEiGQOZHpGpizFH1eEFd|fc_0ce5441d29b9f05d016a5b41d05ff4819bb2b293f882a3a969","toolName":"read","startedAt":"2026-07-18T09:05:20.637Z","args":{"path":"crates/spt-term/src/stream.rs"},"intent":"Reading PTY stream contract"},"id":"b7d5181b","parentId":"c7b47184","timestamp":"2026-07-18T09:05:20.637Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_IlWjgHNMKThATSDQvls4e1Bp|fc_0ce5441d29b9f05d016a5b41d06004819baf52732eea6d8a6b","toolName":"read","startedAt":"2026-07-18T09:05:20.637Z","args":{"path":"crates/spt-term/src/surface.rs"},"intent":"Reading PTY surface layer"},"id":"b3292143","parentId":"b7d5181b","timestamp":"2026-07-18T09:05:20.637Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MXrX6rulaYS3WBT720C07POH|fc_0ce5441d29b9f05d016a5b41d0600c819ba216b10e7ef210a7","toolName":"read","startedAt":"2026-07-18T09:05:20.637Z","args":{"path":"crates/spt-term/src/pty.rs"},"intent":"Reading PTY session lifecycle"},"id":"3cf8cedf","parentId":"b3292143","timestamp":"2026-07-18T09:05:20.637Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GtkRDPpCpWeMGpXiHjTMayZn|fc_0ce5441d29b9f05d016a5b41d06010819bb27a59ac4cebe57a","toolName":"read","startedAt":"2026-07-18T09:05:20.637Z","args":{"path":"crates/spt-term/src/projection.rs"},"intent":"Reading terminal projection logic"},"id":"4591a1c7","parentId":"3cf8cedf","timestamp":"2026-07-18T09:05:20.637Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_JNuL2mkAieTgljwpbpZVyqJ6|fc_0ce5441d29b9f05d016a5b41d06018819ba9a5910aacf6eaf6","toolName":"grep","startedAt":"2026-07-18T09:05:20.637Z","args":{"path":"crates/spt-term/src; crates/spt-term/tests"},"intent":"Finding terminal output filters"},"id":"dd11baf8","parentId":"4591a1c7","timestamp":"2026-07-18T09:05:20.637Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_mJRWSlg5c3tpbN7jH47zMHjJ|fc_0ce5441d29b9f05d016a5b41d06020819b97bb8f36893dab02","toolName":"grep","startedAt":"2026-07-18T09:05:20.637Z","args":{"path":"crates/spt-daemon/src/broker.rs; crates/spt-term/src/screen.rs; crates/spt-daemon/tests; crates/spt/tests"},"intent":"Finding screen repaint tests"},"id":"3156deca","parentId":"dd11baf8","timestamp":"2026-07-18T09:05:20.637Z"}
{"type":"message","id":"214e3d60","parentId":"3156deca","timestamp":"2026-07-18T09:05:20.640Z","message":{"role":"toolResult","toolCallId":"call_IlWjgHNMKThATSDQvls4e1Bp|fc_0ce5441d29b9f05d016a5b41d06004819baf52732eea6d8a6b","toolName":"read","content":[{"type":"text","text":"//! The OS-neutral session-surface contract.\n//!\n//! A [`SessionSurface`] is \"anything you can write input to and read output\n//! from\" — the abstraction every hosted agent session is driven through. The\n//! native PTY backend ([`crate::pty::PtySession`]) is the M3a implementation;\n//! M4's network-attached surface and the GUI text pane slot in behind the same\n//! trait later.\n//!\n//! **OS stream-contract neutrality (Spike #4 / #5, binding):** the surface is\n//! modeled as a byte stream with explicit, OS-independent attach/replay\n//! semantics. The trait deliberately exposes *write input* + *resize* only and\n//! says nothing about whether the underlying output is a raw ordered pipe\n//! (`forkpty`) or a repaint-on-resize screen buffer (ConPTY) — output streaming\n//! is a separate, equally OS-neutral concern (A4, [`crate::pty::PtySession`]'s\n//! reader). No backend's stream semantics leak through this contract.\n\nuse portable_pty::PtySize;\n\n/// Window geometry for a surface — OS-neutral (`rows × cols` plus optional pixel\n/// hints some backends fill and others ignore). Mirrors `portable_pty::PtySize`\n/// without leaking the dependency into the trait's public shape.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub struct SurfaceSize {\n    /// Number of text rows.\n    pub rows: u16,\n    /// Number of text columns.\n    pub cols: u16,\n    /// Cell width in pixels (0 = unset; many systems ignore this).\n    pub pixel_width: u16,\n    /// Cell height in pixels (0 = unset; many systems ignore this).\n    pub pixel_height: u16,\n}\n\nimpl SurfaceSize {\n    /// A `rows × cols` geometry with pixel hints left unset.\n    pub const fn new(rows: u16, cols: u16) -> Self {\n        Self {\n            rows,\n            cols,\n            pixel_width: 0,\n            pixel_height: 0,\n        }\n    }\n}\n\nimpl Default for SurfaceSize {\n    /// The conventional 24×80 terminal.\n    fn default() -> Self {\n        Self::new(24, 80)\n    }\n}\n\nimpl From<SurfaceSize> for PtySize {\n    fn from(s: SurfaceSize) -> Self {\n        PtySize {\n            rows: s.rows,\n            cols: s.cols,\n            pixel_width: s.pixel_width,\n            pixel_height: s.pixel_height,\n        }\n    }\n}\n\n/// A session-surface failure. Carries an operator-facing message; never panics.\n#[derive(Debug)]\npub enum SurfaceError {\n    /// The OS refused to allocate a PTY pair.\n    OpenPty(String),\n    /// The OS refused to spawn the child under the PTY.\n    Spawn(String),\n    /// An I/O operation on the surface failed (read/write/resize).\n    Io(std::io::Error),\n    /// The single writer handle was already taken (PTY writers are once-only).\n    WriterTaken,\n    /// The single reader handle was already taken.\n    ReaderTaken,\n}\n\nimpl std::fmt::Display for SurfaceError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            SurfaceError::OpenPty(e) => write!(f, \"failed to open a pty: {e}\"),\n            SurfaceError::Spawn(e) => write!(f, \"failed to spawn a child under the pty: {e}\"),\n            SurfaceError::Io(e) => write!(f, \"surface i/o failed: {e}\"),\n            SurfaceError::WriterTaken => write!(f, \"the surface writer was already taken\"),\n            SurfaceError::ReaderTaken => write!(f, \"the surface reader was already taken\"),\n        }\n    }\n}\n\nimpl std::error::Error for SurfaceError {}\n\n/// Anything a hosted session can be driven through: input goes in, output comes\n/// out, geometry can change. See the module docs for the OS-neutrality contract.\n///\n/// `Send + Sync` so the M3b broker can host the surface behind a shared handle\n/// and drive `write_input` / `resize` from its IPC thread while a separate\n/// reader thread drains output.\n//\n// [impl->REQ-TERM-1]\npub trait SessionSurface: Send + Sync {\n    /// Write raw bytes to the child's input. Escape sequences pass through\n    /// verbatim (the cooked/raw `send_line` / `send_keys` distinction lands on\n    /// the concrete surface in A3). Flushed on return.\n    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError>;\n\n    /// Inform the surface (and thus the child) of a new window geometry. On a\n    /// real PTY this updates the kernel winsize and raises `SIGWINCH`.\n    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError>;\n\n    /// **send-keys** — raw keystroke injection. Bytes go to the child verbatim,\n    /// including control bytes and escape sequences (e.g. `Ctrl-C` = `0x03`, an\n    /// arrow key = `ESC [ C`). A semantic alias of [`SessionSurface::write_input`]\n    /// for the \"type these exact keys\" intent. Ordering is preserved: each call\n    /// is one serialized write.\n    //\n    // [impl->REQ-TERM-2]\n    fn send_keys(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\n        self.write_input(bytes)\n    }\n\n    /// **send-line** — cooked line submission. Appends a carriage return (`\\r`,\n    /// the byte a real Enter keypress emits; the slave line discipline maps it\n    /// to newline where applicable) and writes the whole line in one serialized\n    /// flush, so a concurrent injector can never split the line.\n    //\n    // [impl->REQ-TERM-2]\n    fn send_line(&self, line: &str) -> Result<(), SurfaceError> {\n        let mut buf = Vec::with_capacity(line.len() + 1);\n        buf.extend_from_slice(line.as_bytes());\n        buf.push(b'\\r');\n        self.write_input(&buf)\n    }\n}\n"}],"details":{"displayContent":{"text":"//! The OS-neutral session-surface contract.\r\n//!\r\n//! A [`SessionSurface`] is \"anything you can write input to and read output\r\n//! from\" — the abstraction every hosted agent session is driven through. The\r\n//! native PTY backend ([`crate::pty::PtySession`]) is the M3a implementation;\r\n//! M4's network-attached surface and the GUI text pane slot in behind the same\r\n//! trait later.\r\n//!\r\n//! **OS stream-contract neutrality (Spike #4 / #5, binding):** the surface is\r\n//! modeled as a byte stream with explicit, OS-independent attach/replay\r\n//! semantics. The trait deliberately exposes *write input* + *resize* only and\r\n//! says nothing about whether the underlying output is a raw ordered pipe\r\n//! (`forkpty`) or a repaint-on-resize screen buffer (ConPTY) — output streaming\r\n//! is a separate, equally OS-neutral concern (A4, [`crate::pty::PtySession`]'s\r\n//! reader). No backend's stream semantics leak through this contract.\r\n\r\nuse portable_pty::PtySize;\r\n\r\n/// Window geometry for a surface — OS-neutral (`rows × cols` plus optional pixel\r\n/// hints some backends fill and others ignore). Mirrors `portable_pty::PtySize`\r\n/// without leaking the dependency into the trait's public shape.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub struct SurfaceSize {\r\n    /// Number of text rows.\r\n    pub rows: u16,\r\n    /// Number of text columns.\r\n    pub cols: u16,\r\n    /// Cell width in pixels (0 = unset; many systems ignore this).\r\n    pub pixel_width: u16,\r\n    /// Cell height in pixels (0 = unset; many systems ignore this).\r\n    pub pixel_height: u16,\r\n}\r\n\r\nimpl SurfaceSize {\r\n    /// A `rows × cols` geometry with pixel hints left unset.\r\n    pub const fn new(rows: u16, cols: u16) -> Self {\r\n        Self {\r\n            rows,\r\n            cols,\r\n            pixel_width: 0,\r\n            pixel_height: 0,\r\n        }\r\n    }\r\n}\r\n\r\nimpl Default for SurfaceSize {\r\n    /// The conventional 24×80 terminal.\r\n    fn default() -> Self {\r\n        Self::new(24, 80)\r\n    }\r\n}\r\n\r\nimpl From<SurfaceSize> for PtySize {\r\n    fn from(s: SurfaceSize) -> Self {\r\n        PtySize {\r\n            rows: s.rows,\r\n            cols: s.cols,\r\n            pixel_width: s.pixel_width,\r\n            pixel_height: s.pixel_height,\r\n        }\r\n    }\r\n}\r\n\r\n/// A session-surface failure. Carries an operator-facing message; never panics.\r\n#[derive(Debug)]\r\npub enum SurfaceError {\r\n    /// The OS refused to allocate a PTY pair.\r\n    OpenPty(String),\r\n    /// The OS refused to spawn the child under the PTY.\r\n    Spawn(String),\r\n    /// An I/O operation on the surface failed (read/write/resize).\r\n    Io(std::io::Error),\r\n    /// The single writer handle was already taken (PTY writers are once-only).\r\n    WriterTaken,\r\n    /// The single reader handle was already taken.\r\n    ReaderTaken,\r\n}\r\n\r\nimpl std::fmt::Display for SurfaceError {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        match self {\r\n            SurfaceError::OpenPty(e) => write!(f, \"failed to open a pty: {e}\"),\r\n            SurfaceError::Spawn(e) => write!(f, \"failed to spawn a child under the pty: {e}\"),\r\n            SurfaceError::Io(e) => write!(f, \"surface i/o failed: {e}\"),\r\n            SurfaceError::WriterTaken => write!(f, \"the surface writer was already taken\"),\r\n            SurfaceError::ReaderTaken => write!(f, \"the surface reader was already taken\"),\r\n        }\r\n    }\r\n}\r\n\r\nimpl std::error::Error for SurfaceError {}\r\n\r\n/// Anything a hosted session can be driven through: input goes in, output comes\r\n/// out, geometry can change. See the module docs for the OS-neutrality contract.\r\n///\r\n/// `Send + Sync` so the M3b broker can host the surface behind a shared handle\r\n/// and drive `write_input` / `resize` from its IPC thread while a separate\r\n/// reader thread drains output.\r\n//\r\n// [impl->REQ-TERM-1]\r\npub trait SessionSurface: Send + Sync {\r\n    /// Write raw bytes to the child's input. Escape sequences pass through\r\n    /// verbatim (the cooked/raw `send_line` / `send_keys` distinction lands on\r\n    /// the concrete surface in A3). Flushed on return.\r\n    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError>;\r\n\r\n    /// Inform the surface (and thus the child) of a new window geometry. On a\r\n    /// real PTY this updates the kernel winsize and raises `SIGWINCH`.\r\n    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError>;\r\n\r\n    /// **send-keys** — raw keystroke injection. Bytes go to the child verbatim,\r\n    /// including control bytes and escape sequences (e.g. `Ctrl-C` = `0x03`, an\r\n    /// arrow key = `ESC [ C`). A semantic alias of [`SessionSurface::write_input`]\r\n    /// for the \"type these exact keys\" intent. Ordering is preserved: each call\r\n    /// is one serialized write.\r\n    //\r\n    // [impl->REQ-TERM-2]\r\n    fn send_keys(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\r\n        self.write_input(bytes)\r\n    }\r\n\r\n    /// **send-line** — cooked line submission. Appends a carriage return (`\\r`,\r\n    /// the byte a real Enter keypress emits; the slave line discipline maps it\r\n    /// to newline where applicable) and writes the whole line in one serialized\r\n    /// flush, so a concurrent injector can never split the line.\r\n    //\r\n    // [impl->REQ-TERM-2]\r\n    fn send_line(&self, line: &str) -> Result<(), SurfaceError> {\r\n        let mut buf = Vec::with_capacity(line.len() + 1);\r\n        buf.extend_from_slice(line.as_bytes());\r\n        buf.push(b'\\r');\r\n        self.write_input(&buf)\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\surface.rs"}}},"isError":false,"timestamp":1784365520640}}
{"type":"message","id":"22072770","parentId":"214e3d60","timestamp":"2026-07-18T09:05:20.648Z","message":{"role":"toolResult","toolCallId":"call_jBbFhEiGQOZHpGpizFH1eEFd|fc_0ce5441d29b9f05d016a5b41d05ff4819bb2b293f882a3a969","toolName":"read","content":[{"type":"text","text":"//! Bounded byte-stream local streaming (M3a A4).\n//!\n//! [`OutputStream`] is the v1 consumer shape: the A2 drain forwards every byte\n//! into a bounded `sync_channel`, and a consumer pulls chunks off the other end.\n//! When the consumer falls behind, the channel fills, the drain's `send` blocks,\n//! the reader stops draining, the PTY buffer fills, and the child blocks on\n//! write — **end-to-end bounded backpressure, no unbounded buffer**. The off-node\n//! hop (M4 transport) and the predictive \"feels-local\" layer ride on top of this\n//! substrate later; M3a delivers the local shape only.\n//!\n//! **OS-neutral (Spike #4 / #5):** the stream is bytes, nothing more. It makes no\n//! ordering promise beyond \"every byte the reader forwarded, in the order it was\n//! forwarded\" — on ConPTY a repaint-on-resize legitimately reorders/duplicates\n//! content, so consumers (e.g. the A5 digest) match on *value-set coverage*, not\n//! byte-exact sequence.\n\nuse std::sync::mpsc::{sync_channel, Receiver};\nuse std::time::Duration;\n\nuse crate::pty::PtySession;\nuse crate::reader::Drain;\nuse crate::surface::SurfaceError;\n\n/// A bounded stream of the child's output chunks. Dropping it unblocks a drain\n/// that is parked on a full channel (the blocked `send` returns once the\n/// receiver is gone), letting the pump wind down once the child is torn down.\npub struct OutputStream {\n    rx: Receiver<Vec<u8>>,\n    drain: Drain,\n}\n\nimpl OutputStream {\n    /// Block for the next output chunk. Returns `None` once the stream has ended\n    /// (the drain thread exited and dropped its sender — i.e. the child's output\n    /// reached EOF or the pump was stopped).\n    pub fn recv(&self) -> Option<Vec<u8>> {\n        self.rx.recv().ok()\n    }\n\n    /// Non-blocking poll for the next chunk, if one is already buffered.\n    pub fn try_recv(&self) -> Option<Vec<u8>> {\n        self.rx.try_recv().ok()\n    }\n\n    /// Block up to `timeout` for the next chunk.\n    pub fn recv_timeout(&self, timeout: Duration) -> Option<Vec<u8>> {\n        self.rx.recv_timeout(timeout).ok()\n    }\n\n    /// The underlying drain — live `bytes_forwarded` / `dsr_answered` counters\n    /// and the cooperative stop flag.\n    pub fn drain(&self) -> &Drain {\n        &self.drain\n    }\n\n    /// Ask the pump to stop (cooperative — see [`Drain::request_stop`]).\n    pub fn request_stop(&self) {\n        self.drain.request_stop();\n    }\n}\n\nimpl PtySession {\n    /// Stream this surface's output through a bounded channel of `capacity`\n    /// chunks. `capacity == 0` is a rendezvous channel (maximum backpressure:\n    /// every chunk blocks until the consumer takes it).\n    ///\n    /// Takes the master reader (once). The returned [`OutputStream`] is the sole\n    /// consumer; chunks the consumer never takes apply backpressure rather than\n    /// growing an unbounded buffer.\n    //\n    // [impl->REQ-TERM-3]\n    pub fn byte_stream(&self, capacity: usize) -> Result<OutputStream, SurfaceError> {\n        let (tx, rx) = sync_channel::<Vec<u8>>(capacity);\n        let drain = self.drain(move |chunk| {\n            // Bounded send: blocks when `capacity` chunks are unconsumed\n            // (backpressure); drops silently once the receiver is gone so a\n            // torn-down consumer never wedges the pump.\n            let _ = tx.send(chunk.to_vec());\n        })?;\n        Ok(OutputStream { rx, drain })\n    }\n}\n"}],"details":{"displayContent":{"text":"//! Bounded byte-stream local streaming (M3a A4).\r\n//!\r\n//! [`OutputStream`] is the v1 consumer shape: the A2 drain forwards every byte\r\n//! into a bounded `sync_channel`, and a consumer pulls chunks off the other end.\r\n//! When the consumer falls behind, the channel fills, the drain's `send` blocks,\r\n//! the reader stops draining, the PTY buffer fills, and the child blocks on\r\n//! write — **end-to-end bounded backpressure, no unbounded buffer**. The off-node\r\n//! hop (M4 transport) and the predictive \"feels-local\" layer ride on top of this\r\n//! substrate later; M3a delivers the local shape only.\r\n//!\r\n//! **OS-neutral (Spike #4 / #5):** the stream is bytes, nothing more. It makes no\r\n//! ordering promise beyond \"every byte the reader forwarded, in the order it was\r\n//! forwarded\" — on ConPTY a repaint-on-resize legitimately reorders/duplicates\r\n//! content, so consumers (e.g. the A5 digest) match on *value-set coverage*, not\r\n//! byte-exact sequence.\r\n\r\nuse std::sync::mpsc::{sync_channel, Receiver};\r\nuse std::time::Duration;\r\n\r\nuse crate::pty::PtySession;\r\nuse crate::reader::Drain;\r\nuse crate::surface::SurfaceError;\r\n\r\n/// A bounded stream of the child's output chunks. Dropping it unblocks a drain\r\n/// that is parked on a full channel (the blocked `send` returns once the\r\n/// receiver is gone), letting the pump wind down once the child is torn down.\r\npub struct OutputStream {\r\n    rx: Receiver<Vec<u8>>,\r\n    drain: Drain,\r\n}\r\n\r\nimpl OutputStream {\r\n    /// Block for the next output chunk. Returns `None` once the stream has ended\r\n    /// (the drain thread exited and dropped its sender — i.e. the child's output\r\n    /// reached EOF or the pump was stopped).\r\n    pub fn recv(&self) -> Option<Vec<u8>> {\r\n        self.rx.recv().ok()\r\n    }\r\n\r\n    /// Non-blocking poll for the next chunk, if one is already buffered.\r\n    pub fn try_recv(&self) -> Option<Vec<u8>> {\r\n        self.rx.try_recv().ok()\r\n    }\r\n\r\n    /// Block up to `timeout` for the next chunk.\r\n    pub fn recv_timeout(&self, timeout: Duration) -> Option<Vec<u8>> {\r\n        self.rx.recv_timeout(timeout).ok()\r\n    }\r\n\r\n    /// The underlying drain — live `bytes_forwarded` / `dsr_answered` counters\r\n    /// and the cooperative stop flag.\r\n    pub fn drain(&self) -> &Drain {\r\n        &self.drain\r\n    }\r\n\r\n    /// Ask the pump to stop (cooperative — see [`Drain::request_stop`]).\r\n    pub fn request_stop(&self) {\r\n        self.drain.request_stop();\r\n    }\r\n}\r\n\r\nimpl PtySession {\r\n    /// Stream this surface's output through a bounded channel of `capacity`\r\n    /// chunks. `capacity == 0` is a rendezvous channel (maximum backpressure:\r\n    /// every chunk blocks until the consumer takes it).\r\n    ///\r\n    /// Takes the master reader (once). The returned [`OutputStream`] is the sole\r\n    /// consumer; chunks the consumer never takes apply backpressure rather than\r\n    /// growing an unbounded buffer.\r\n    //\r\n    // [impl->REQ-TERM-3]\r\n    pub fn byte_stream(&self, capacity: usize) -> Result<OutputStream, SurfaceError> {\r\n        let (tx, rx) = sync_channel::<Vec<u8>>(capacity);\r\n        let drain = self.drain(move |chunk| {\r\n            // Bounded send: blocks when `capacity` chunks are unconsumed\r\n            // (backpressure); drops silently once the receiver is gone so a\r\n            // torn-down consumer never wedges the pump.\r\n            let _ = tx.send(chunk.to_vec());\r\n        })?;\r\n        Ok(OutputStream { rx, drain })\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\stream.rs"}}},"isError":false,"timestamp":1784365520648}}
{"type":"message","id":"8c5569c2","parentId":"22072770","timestamp":"2026-07-18T09:05:20.665Z","message":{"role":"toolResult","toolCallId":"call_JNuL2mkAieTgljwpbpZVyqJ6|fc_0ce5441d29b9f05d016a5b41d06018819ba9a5910aacf6eaf6","toolName":"grep","content":[{"type":"text","text":"# crates/spt-term/\n\n## src/\n### digest.rs\n 57|/// authoritative dedup + cursor key, `(ledger_ordinal << 32) | line_idx` for a\n*58|/// committed entry, blanked to `None` for an open/partial turn — and an optional\n 59|/// `ts`. `Boundary`/`Context` are spt-injected (not transcript records), so they\n 60|/// carry no `seq`, only their `ts`.\n 61|#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n...\n 87|    },\n*88|    /// A session boundary marker (REQ-TERM-6): a `/clear` or `/compact` between\n*89|    /// two spanned sessions, rendered as a distinctive divider so a reader sees\n*90|    /// the context reset without losing the thread. `kind` is the trigger\n*91|    /// (`clear` | `compact` | `boot`); `ts` is its RFC3339-UTC instant if known.\n 92|    /// Carried in a preamble turn (input `None`) the spanning projection splices\n 93|    /// between sessions — never produced by the single-session contract fold.\n 94|    Boundary {\n*95|        /// The boundary trigger token (`clear` | `compact` | `boot`).\n 96|        kind: String,\n 97|        /// The boundary's RFC3339-UTC instant, if recorded in the ledger.\n 98|        ts: Option<String>,\n...\n 118|    /// contract fold / test paths that have no source ordinal (seq/ts stay `None`;\n*119|    /// the daemon timeline threads them in afterward). [impl->REQ-DIGEST-CURSOR]\n 120|    pub fn agent(text: String) -> Self {\n 121|        DigestEntry::Agent {\n 122|            text,\n...\n 148|    /// (REQ-DIGEST-CURSOR). `None` for a preamble turn (no input) or a partial\n*149|    /// turn (its seqs are blanked).\n 150|    #[serde(skip_serializing_if = \"Option::is_none\")]\n 151|    pub input_seq: Option<u64>,\n 152|    /// True for the trailing **open** turn — one no later input has closed. A\n*153|    /// partial turn carries no stable seq (its entries' seqs are blanked); all\n 154|    /// prior input-closed turns are committed. [impl->REQ-DIGEST-CURSOR]\n 155|    #[serde(default, skip_serializing_if = \"std::ops::Not::not\")]\n 156|    pub partial: bool,\n### lib.rs\n 19|//! pipe**; ConPTY is a **screen buffer** (repaint-on-resize reorders + duplicates\n*20|//! the stream; a mid-session attach sees the viewport, not scrollback). The\n 21|//! [`SessionSurface`] trait and the byte-stream / digest contracts therefore must\n 22|//! **not** bake in either OS's semantics — the surface is modeled as a byte stream\n 23|//! with explicit, OS-independent attach/replay semantics.\n...\n 30|//   A1  SessionSurface trait + ConPTY/forkpty backends     (REQ-TERM-1)       ← landed\n*31|//   A2  ConPTY-DSR auto-answer drain thread                 (REQ-HAZARD-CONPTY-DSR) ← landed\n 32|//   A3  send-keys / send-line injection                     (REQ-TERM-2)\n 33|//   A4  byte-stream local streaming (bounded backpressure)  (REQ-TERM-3)       ← landed\n 34|//   A5  live activity buffer — digest output types + the log-source projection\n...\n 40|pub mod pty;\n*41|pub mod reader;\n 42|pub mod screen;\n 43|pub mod stream;\n 44|pub mod surface;\n...\n 50|    record_to_tagged, record_to_tagged_result, window_input_turns, ContextRecord,\n*51|    DigestDiagnostics, DigestRecord, DigestRole, DropReason, DroppedLine, TimelineItem, ToolRef,\n 52|};\n 53|pub use portable_pty::CommandBuilder;\n 54|pub use pty::PtySession;\n*55|pub use reader::Drain;\n 56|// [impl->REQ-BROKER-SCREEN-GRID] the server-side render grid + clean repaint on\n 57|// attach (ADR-0031) — a clean-room `vte::Perform` interpreter of the byte stream.\n 58|pub use screen::ScreenGrid;\n*59|pub use stream::OutputStream;\n 60|pub use surface::{SessionSurface, SurfaceError, SurfaceSize};\n### projection.rs\n 9|//! the semantic turn boundaries in repaint traffic. The split (CONTEXT.md\n*10|//! \"session digest\"): **render surfaces read the PTY** (the raw byte ring,\n*11|//! unchanged); **content surfaces read the logs.** The digest is a rolling\n 12|//! tail-projection of the **same normalized history the echo-commune consumes**\n 13|//! (history subsystem Path A locate+normalize / Path B native store) — one\n 14|//! pipeline, two consumers.\n...\n 40|//! **Forward-compat:** unknown object fields are ignored (a future contract field\n*41|//! does not break an older reader); a line that is not a valid record — malformed\n 42|//! JSON, missing/unknown `role`, or a `tool` record without its `tool` object —\n 43|//! is **skipped**, never a panic and never an aborted projection.\n 44|//!\n...\n 107|\n*108|/// Why a normalized line was dropped from the projection — the skip-diagnostics\n*109|/// (ADR-0019). The silent `from_str().ok()` is gone: every drop now carries a\n 110|/// reason a `digest-proof` run (or the runtime warn) can surface.\n 111|#[derive(Debug, Clone, PartialEq, Eq)]\n*112|pub enum DropReason {\n 113|    /// Not valid contract JSON: malformed, a non-object, or an unknown/missing\n 114|    /// `role` (serde's message — e.g. \"unknown variant `whisper`\" — is carried).\n 115|    Malformed(String),\n...\n 119|\n*120|impl std::fmt::Display for DropReason {\n 121|    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n 122|        match self {\n*123|            DropReason::Malformed(msg) => write!(f, \"not a valid contract record: {msg}\"),\n*124|            DropReason::ToolMissingObject => {\n 125|                write!(f, \"role \\\"tool\\\" without a `tool` object\")\n 126|            }\n 127|        }\n...\n 130|\n*131|/// One dropped line, with its 1-based position, raw text, and reason — the unit\n 132|/// of the skip-diagnostics surfaced by `spt adapter digest-proof`.\n 133|#[derive(Debug, Clone, PartialEq, Eq)]\n*134|pub struct DroppedLine {\n 135|    pub line_no: usize,\n 136|    pub raw: String,\n*137|    pub reason: DropReason,\n 138|}\n 139|\n 140|/// Skip-diagnostics for one projection: how many records parsed and every line\n*141|/// that was dropped (with why). The author-time + runtime answer to \"the digest\n 142|/// returns nothing\" (ADR-0019 §2).\n 143|#[derive(Debug, Clone, Default, PartialEq, Eq)]\n 144|pub struct DigestDiagnostics {\n...\n 146|    pub parsed: usize,\n*147|    /// Every dropped line, in source order.\n*148|    pub dropped: Vec<DroppedLine>,\n 149|}\n 150|\n 151|impl DigestDiagnostics {\n*152|    /// Number of dropped lines.\n*153|    pub fn drop_count(&self) -> usize {\n*154|        self.dropped.len()\n 155|    }\n*156|    /// The first dropped line (the \"first mismatch\" an author chases).\n*157|    pub fn first_drop(&self) -> Option<&DroppedLine> {\n### pty.rs\n 4|//!\n*5|//! [`PtySession`] spawns a child under a real PTY, owns the master reader and\n 6|//! writer, and serves the [`SessionSurface`] contract. It is **single-surface\n 7|//! per handle** — one child's PTY, not the multiplexed fleet (that supervisor is\n 8|//! the M3b broker).\n...\n 11|//!\n*12|//! `resize()` races the reader thread on Windows; the master is therefore held\n 13|//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared\n*14|//! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup\n 15|//! cursor query through the same serialized writer the caller injects input\n 16|//! through — no torn writes.\n 17|\n*18|use std::io::{Read, Write};\n 19|use std::sync::{Arc, Mutex};\n 20|\n 21|use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};\n...\n 24|\n*25|/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain\n*26|/// thread so the DSR auto-answer and caller input never interleave mid-write.\n 27|pub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;\n 28|\n 29|/// A child process running under a native PTY, exposed as a [`SessionSurface`].\n...\n 31|/// Build one with [`PtySession::spawn`]; drive input via [`SessionSurface::write_input`]\n*32|/// (or the A3 `send_keys`/`send_line` helpers), drain output via\n*33|/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /\n 34|/// [`PtySession::wait`] / [`PtySession::kill`].\n 35|pub struct PtySession {\n*36|    /// The master end, behind a lock so `resize` never races the reader (Spike #5).\n 37|    master: Mutex<Box<dyn MasterPty + Send>>,\n*38|    /// The single PTY writer, shared so the A2 drain thread can answer DSR.\n 39|    writer: SharedWriter,\n 40|    /// The spawned child; behind a lock for `wait`/`try_wait`.\n 41|    child: Mutex<Box<dyn Child + Send + Sync>>,\n 42|    /// A kill handle split from the child so we can terminate from another\n*43|    /// thread even while `wait` blocks (`portable-pty` `ChildKiller`).\n 44|    killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,\n*45|    /// The master reader, taken at most once (`take_reader`).\n*46|    reader: Mutex<Option<Box<dyn Read + Send>>>,\n 47|    /// The child OS pid, captured at spawn.\n 48|    pid: Option<u32>,\n 49|}\n...\n 53|    ///\n*54|    /// The slave end is dropped immediately after the child is spawned: the\n 55|    /// child holds the only remaining slave handle, which lets EOF propagate on\n 56|    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF\n*57|    /// behaviour is governed by the writer, handled by the A2 drain discipline.)\n 58|    //\n 59|    // [impl->REQ-TERM-1]\n 60|    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {\n...\n 69|            .map_err(|e| SurfaceError::Spawn(e.to_string()))?;\n*70|        // Drop the slave so the child owns the only slave-side handle.\n*71|        drop(pair.slave);\n 72|\n*73|        let reader = pair\n 74|            .master\n*75|            .try_clone_reader()\n 76|            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n 77|        let writer = pair\n 78|            .master\n...\n 88|            killer: Mutex::new(killer),\n*89|            reader: Mutex::new(Some(reader)),\n 90|            pid,\n 91|        })\n 92|    }\n### reader.rs\n*1|//! Output drain pump — the ConPTY-DSR auto-answer reader (M3a A2).\n 2|//!\n 3|//! ConPTY can emit a cursor-position query `ESC [ 6 n` and **withhold all child\n 4|//! stdout until the terminal answers it** (Spike #1: a run captured exactly the\n...\n 15|//! **No-EOF-while-writer-held (Spike #1 secondary gotcha):** a ConPTY master\n*16|//! does *not* EOF while the writer is held, so the read loop runs on its own\n*17|//! thread and shutdown **never gates on a blocking `read()` returning**. The\n 18|//! broker stops a surface by terminating the child (which tears the PTY down and\n*19|//! unblocks the read); the [`Drain`] stop flag is a cooperative secondary signal\n*20|//! observed whenever a read does return. Deterministic, ordered teardown (drop\n 21|//! the writer, then the master) is the M3b broker's lifecycle responsibility.\n 22|\n*23|use std::io::{Read, Write};\n 24|use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\n 25|use std::sync::Arc;\n*26|use std::thread;\n 27|\n 28|use crate::pty::{PtySession, SharedWriter};\n 29|use crate::surface::SurfaceError;\n...\n 38|/// Streaming matcher for [`DSR_QUERY`] that survives chunk boundaries (the 4-byte\n*39|/// query can split across two `read()`s). `DSR_QUERY` has no proper prefix that\n 40|/// is also a suffix, so a single \"bytes matched so far\" counter is sufficient.\n 41|#[derive(Default)]\n 42|struct DsrMatcher {\n...\n 65|\n*66|/// A running drain pump over a surface's output. Holds live counters and a\n 67|/// cooperative stop flag; see the module docs for the teardown contract.\n*68|pub struct Drain {\n 69|    stop: Arc<AtomicBool>,\n 70|    dsr_answered: Arc<AtomicU64>,\n 71|    bytes: Arc<AtomicU64>,\n*72|    handle: Option<thread::JoinHandle<()>>,\n 73|}\n 74|\n*75|impl Drain {\n 76|    /// Total DSR queries answered so far (live).\n 77|    pub fn dsr_answered(&self) -> u64 {\n 78|        self.dsr_answered.load(Ordering::Relaxed)\n...\n 85|\n*86|    /// Ask the pump to stop. Observed the next time a `read()` returns; it does\n*87|    /// **not** interrupt a read already blocked (that requires tearing the child\n 88|    /// down — the no-EOF-while-writer-held discipline).\n 89|    pub fn request_stop(&self) {\n 90|        self.stop.store(true, Ordering::Relaxed);\n...\n 92|\n*93|    /// Join the pump thread. Only returns once the underlying `read()` has\n*94|    /// unblocked (EOF / error / a returned read that observes the stop flag) —\n 95|    /// on ConPTY that means the child must be torn down first. Best-effort.\n 96|    pub fn join(mut self) {\n 97|        if let Some(h) = self.handle.take() {\n...\n 102|\n*103|impl Drain {\n*104|    /// Spawn the drain thread over `reader`, answering DSR via `writer` and\n 105|    /// forwarding every byte to `sink`. The `sink` may block (that is how A4's\n 106|    /// bounded channel applies backpressure).\n*107|    fn spawn<R, S>(reader: R, writer: SharedWriter, mut sink: S) -> Self\n 108|    where\n*109|        R: Read + Send + 'static,\n 110|        S: FnMut(&[u8]) + Send + 'static,\n 111|    {\n 112|        let stop = Arc::new(AtomicBool::new(false));\n### screen.rs\n 11|//!\n*12|//! **Scope (ratified, ADR-0031):** current screen only — the visible viewport\n 13|//! (cell buffer + cursor + main/alt buffer + the modes a repaint needs). Scrollback\n 14|//! stays the raw ring (history). The grid drives ONLY the attach repaint; live\n 15|//! frames after attach are the unchanged raw bytes.\n...\n 141|impl Cell {\n*142|    /// A cell that renders as nothing on a freshly-cleared screen: a space with the\n*143|    /// default pen. Trailing runs of these are trimmed from a repaint.\n*144|    fn is_default_blank(&self) -> bool {\n 145|        self.ch == ' ' && self.pen == Pen::default()\n 146|    }\n 147|}\n...\n 149|/// The public screen grid: a `vte` parser paired with the clean-room [`GridState`]\n*150|/// it drives. Single-writer by construction (the broker's session drain thread) —\n 151|/// mirrors the existing single-producer ring (ADR-0031, hazard 7.12).\n 152|pub struct ScreenGrid {\n 153|    parser: Parser,\n...\n 157|impl ScreenGrid {\n*158|    /// A fresh grid sized `rows × cols` (a blank main screen, cursor home).\n 159|    pub fn new(rows: u16, cols: u16) -> Self {\n 160|        ScreenGrid {\n 161|            parser: Parser::new(),\n...\n 166|    /// Feed a raw output chunk through the parser, advancing the current screen.\n*167|    /// Called on the session drain path AFTER the ring append + fan-out, so it adds\n 168|    /// no latency to controller/viewer delivery (ADR-0031, REQ-HAZARD-INJECT-CONTROL-COEXIST).\n 169|    pub fn advance(&mut self, bytes: &[u8]) {\n 170|        for &b in bytes {\n...\n 183|    /// Synthesize a clean repaint of the CURRENT screen: a byte sequence that,\n*184|    /// applied to a fresh terminal, reproduces the visible viewport (alt/main\n 185|    /// buffer, cells with minimal-ish SGR runs, cursor position + visibility) with\n 186|    /// NO raw scrollback and NO mid-stream rendering debris. This replaces the\n 187|    /// raw-ring initial batch at `become_controller` / `add_viewer`.\n...\n 211|    /// cold attach carries the CURRENT title, not the attacher's stale one (E-2).\n*212|    /// Cleared by RIS (full_reset rebuilds the state); survives resize.\n 213|    // [impl->REQ-BROKER-SCREEN-GRID]\n 214|    title: Option<String>,\n 215|}\n...\n 223|            cols,\n*224|            main: blank_buffer(rows, cols),\n*225|            alt: blank_buffer(rows, cols),\n 226|            alt_active: false,\n 227|            row: 0,\n 228|            col: 0,\n...\n 254|\n*255|    /// The pen an erase fills with: attributes cleared but the current background\n*256|    /// preserved (so a status-bar erase keeps its colour).\n*257|    fn erase_pen(&self) -> Pen {\n 258|        Pen {\n 259|            bg: self.pen.bg,\n 260|            ..Pen::default()\n...\n 263|\n*264|    fn blank(&self) -> Cell {\n 265|        Cell {\n 266|            ch: ' ',\n*267|            pen: self.erase_pen(),\n 268|        }\n 269|    }\n 270|\n...\n 299|    fn scroll_up(&mut self, n: usize) {\n*300|        let blank = self.blank();\n 301|        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n 302|        let cols = self.cols;\n 303|        let buf = self.buf_mut();\n...\n 306|                buf[top..=bottom].rotate_left(1);\n*307|                buf[bottom] = vec![blank; cols];\n 308|            }\n 309|        }\n 310|    }\n...\n 312|    fn scroll_down(&mut self, n: usize) {\n*313|        let blank = self.blank();\n 314|        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n 315|        let cols = self.cols;\n 316|        let buf = self.buf_mut();\n...\n 319|                buf[top..=bottom].rotate_right(1);\n*320|                buf[top] = vec![blank; cols];\n 321|            }\n 322|        }\n 323|    }\n### stream.rs\n 2|//!\n*3|//! [`OutputStream`] is the v1 consumer shape: the A2 drain forwards every byte\n 4|//! into a bounded `sync_channel`, and a consumer pulls chunks off the other end.\n*5|//! When the consumer falls behind, the channel fills, the drain's `send` blocks,\n*6|//! the reader stops draining, the PTY buffer fills, and the child blocks on\n 7|//! write — **end-to-end bounded backpressure, no unbounded buffer**. The off-node\n 8|//! hop (M4 transport) and the predictive \"feels-local\" layer ride on top of this\n 9|//! substrate later; M3a delivers the local shape only.\n...\n 11|//! **OS-neutral (Spike #4 / #5):** the stream is bytes, nothing more. It makes no\n*12|//! ordering promise beyond \"every byte the reader forwarded, in the order it was\n 13|//! forwarded\" — on ConPTY a repaint-on-resize legitimately reorders/duplicates\n 14|//! content, so consumers (e.g. the A5 digest) match on *value-set coverage*, not\n 15|//! byte-exact sequence.\n...\n 20|use crate::pty::PtySession;\n*21|use crate::reader::Drain;\n 22|use crate::surface::SurfaceError;\n 23|\n*24|/// A bounded stream of the child's output chunks. Dropping it unblocks a drain\n 25|/// that is parked on a full channel (the blocked `send` returns once the\n 26|/// receiver is gone), letting the pump wind down once the child is torn down.\n*27|pub struct OutputStream {\n 28|    rx: Receiver<Vec<u8>>,\n*29|    drain: Drain,\n 30|}\n 31|\n*32|impl OutputStream {\n 33|    /// Block for the next output chunk. Returns `None` once the stream has ended\n*34|    /// (the drain thread exited and dropped its sender — i.e. the child's output\n 35|    /// reached EOF or the pump was stopped).\n 36|    pub fn recv(&self) -> Option<Vec<u8>> {\n 37|        self.rx.recv().ok()\n...\n 39|\n*40|    /// Non-blocking poll for the next chunk, if one is already buffered.\n 41|    pub fn try_recv(&self) -> Option<Vec<u8>> {\n 42|        self.rx.try_recv().ok()\n 43|    }\n...\n 49|\n*50|    /// The underlying drain — live `bytes_forwarded` / `dsr_answered` counters\n 51|    /// and the cooperative stop flag.\n*52|    pub fn drain(&self) -> &Drain {\n*53|        &self.drain\n 54|    }\n 55|\n*56|    /// Ask the pump to stop (cooperative — see [`Drain::request_stop`]).\n 57|    pub fn request_stop(&self) {\n*58|        self.drain.request_stop();\n 59|    }\n 60|}\n 61|\n...\n 66|    ///\n*67|    /// Takes the master reader (once). The returned [`OutputStream`] is the sole\n 68|    /// consumer; chunks the consumer never takes apply backpressure rather than\n 69|    /// growing an unbounded buffer.\n 70|    //\n 71|    // [impl->REQ-TERM-3]\n*72|    pub fn byte_stream(&self, capacity: usize) -> Result<OutputStream, SurfaceError> {\n 73|        let (tx, rx) = sync_channel::<Vec<u8>>(capacity);\n*74|        let drain = self.drain(move |chunk| {\n 75|            // Bounded send: blocks when `capacity` chunks are unconsumed\n*76|            // (backpressure); drops silently once the receiver is gone so a\n 77|            // torn-down consumer never wedges the pump.\n 78|            let _ = tx.send(chunk.to_vec());\n 79|        })?;\n### surface.rs\n 2|//!\n*3|//! A [`SessionSurface`] is \"anything you can write input to and read output\n 4|//! from\" — the abstraction every hosted agent session is driven through. The\n 5|//! native PTY backend ([`crate::pty::PtySession`]) is the M3a implementation;\n 6|//! M4's network-attached surface and the GUI text pane slot in behind the same\n...\n 14|//! is a separate, equally OS-neutral concern (A4, [`crate::pty::PtySession`]'s\n*15|//! reader). No backend's stream semantics leak through this contract.\n 16|\n 17|use portable_pty::PtySize;\n 18|\n...\n 70|    Spawn(String),\n*71|    /// An I/O operation on the surface failed (read/write/resize).\n 72|    Io(std::io::Error),\n*73|    /// The single writer handle was already taken (PTY writers are once-only).\n 74|    WriterTaken,\n*75|    /// The single reader handle was already taken.\n*76|    ReaderTaken,\n 77|}\n 78|\n 79|impl std::fmt::Display for SurfaceError {\n...\n 84|            SurfaceError::Io(e) => write!(f, \"surface i/o failed: {e}\"),\n*85|            SurfaceError::WriterTaken => write!(f, \"the surface writer was already taken\"),\n*86|            SurfaceError::ReaderTaken => write!(f, \"the surface reader was already taken\"),\n 87|        }\n 88|    }\n 89|}\n...\n 96|/// `Send + Sync` so the M3b broker can host the surface behind a shared handle\n*97|/// and drive `write_input` / `resize` from its IPC thread while a separate\n*98|/// reader thread drains output.\n 99|//\n 100|// [impl->REQ-TERM-1]\n 101|pub trait SessionSurface: Send + Sync {\n### winprog.rs\n 90|    seg.rfind('.')\n*91|        .filter(|&i| i > 0)\n 92|        .map(|i| seg[i + 1..].to_ascii_lowercase())\n 93|}\n 94|\n...\n 117|/// - An explicit extension ⇒ classify by it (no search).\n*118|/// - A bare name ⇒ search every `dir` × `exts` (PATHEXT order already prefers\n 119|///   `.EXE` over `.CMD`), then an extensionless fallback, then passthrough.\n 120|/// - A separator-bearing extensionless path ⇒ search its own dir × `exts`, then\n 121|///   the literal path, then passthrough.\n...\n 172|        .split(';')\n*173|        .map(|s| s.trim())\n*174|        .filter(|s| !s.is_empty())\n 175|        .map(|s| {\n 176|            if s.starts_with('.') {\n 177|                s.to_string()\n\n## tests/\n### dsr.rs\n*1|//! A2 regression tests for the ConPTY-DSR auto-answer drain pump.\n 2|//!\n 3|//! Whether the host ConPTY emits the DSR query is build/program-dependent, so\n 4|//! the *auto-answer mechanism* is proven deterministically with a synthetic\n*5|//! query in the `reader` unit tests. These integration tests prove the\n*6|//! complementary end-to-end guarantee on a **real** PTY: the drain forwards the\n 7|//! child's stdout and nothing is withheld (on any ConPTY that does stall, the\n 8|//! auto-answer is what keeps this passing).\n 9|\n 10|use std::sync::{Arc, Mutex};\n*11|use std::thread;\n 12|use std::time::Duration;\n 13|\n 14|use spt_term::{PtySession, SurfaceSize};\n...\n 38|        }\n*39|        thread::sleep(Duration::from_millis(10));\n 40|    }\n 41|    false\n 42|}\n...\n 45|#[test]\n*46|fn drain_forwards_child_output() {\n*47|    let session = print_marker(\"HELLO-DRAIN\");\n 48|    let collected = Arc::new(Mutex::new(Vec::new()));\n 49|    let sink = Arc::clone(&collected);\n*50|    let drain = session\n*51|        .drain(move |chunk| sink.lock().unwrap().extend_from_slice(chunk))\n*52|        .expect(\"start drain\");\n 53|\n*54|    let found = wait_for(&collected, \"HELLO-DRAIN\");\n*55|    drain.request_stop();\n 56|    let _ = session.kill();\n 57|\n 58|    assert!(\n 59|        found,\n*60|        \"the drain must forward the child's stdout (ConPTY: only after DSR is answered)\"\n 61|    );\n*62|    // The reader is once-only: drain took it.\n*63|    assert!(session.take_reader().is_err(), \"drain owns the reader\");\n 64|}\n 65|\n 66|// [int->REQ-HAZARD-CONPTY-DSR]\n...\n 68|#[test]\n*69|fn conpty_output_not_withheld_under_the_drain() {\n 70|    let session = print_marker(\"DSR-PROOF\");\n 71|    let collected = Arc::new(Mutex::new(Vec::new()));\n 72|    let sink = Arc::clone(&collected);\n*73|    let drain = session\n*74|        .drain(move |chunk| sink.lock().unwrap().extend_from_slice(chunk))\n*75|        .expect(\"start drain\");\n 76|\n*77|    // The drain runs the DSR auto-answer; ConPTY stdout reaches us either way.\n 78|    let found = wait_for(&collected, \"DSR-PROOF\");\n### inject.rs\n 2|//!\n*3|//! An echo child (`cat` on Unix, `findstr \".\"` on Windows) reads stdin and\n 4|//! prints each line back, so injected input round-trips through the real PTY and\n*5|//! into the drained output stream. Ordering is asserted by index; `Ctrl-C`\n 6|//! (`0x03`) must interrupt the child.\n 7|\n 8|use std::sync::{Arc, Mutex};\n*9|use std::thread;\n 10|use std::time::Duration;\n 11|\n*12|use spt_term::{Drain, PtySession, SessionSurface, SurfaceSize};\n 13|\n*14|/// An echo child: reads stdin, writes each line to stdout, OS-appropriate.\n 15|fn echo_child() -> PtySession {\n 16|    #[cfg(unix)]\n 17|    let s = PtySession::spawn_program(\"cat\", Vec::<String>::new(), SurfaceSize::new(24, 80));\n...\n 22|\n*23|fn drain_into(session: &PtySession) -> (Drain, Arc<Mutex<Vec<u8>>>) {\n 24|    let collected = Arc::new(Mutex::new(Vec::new()));\n 25|    let sink = Arc::clone(&collected);\n*26|    let drain = session\n*27|        .drain(move |chunk| sink.lock().unwrap().extend_from_slice(chunk))\n*28|        .expect(\"start drain\");\n*29|    (drain, collected)\n 30|}\n 31|\n 32|fn text(collected: &Arc<Mutex<Vec<u8>>>) -> String {\n...\n 40|        }\n*41|        thread::sleep(Duration::from_millis(10));\n 42|    }\n 43|    false\n 44|}\n...\n 49|    let session = echo_child();\n*50|    let (drain, collected) = drain_into(&session);\n 51|\n 52|    session.send_line(\"INJECT-ALPHA\").expect(\"send first line\");\n 53|    session.send_line(\"INJECT-BRAVO\").expect(\"send second line\");\n...\n 60|    let _ = session.send_keys(&[0x03]);\n*61|    drain.request_stop();\n 62|    let _ = session.kill();\n 63|\n 64|    let t = text(&collected);\n...\n 80|    let session = echo_child();\n*81|    let (drain, collected) = drain_into(&session);\n 82|\n 83|    // Handshake: inject a line and wait for it to echo back. This proves the\n*84|    // child is up and *reading* before we deliver Ctrl-C — otherwise the signal\n*85|    // races child startup and can be dropped before its handler is installed.\n 86|    session\n*87|        .send_line(\"READY-PROBE\")\n*88|        .expect(\"send readiness probe\");\n*89|    let ready = wait_until(&collected, |t| t.contains(\"READY-PROBE\"));\n 90|    assert!(\n*91|        ready,\n 92|        \"echo child must come up and echo before Ctrl-C: {:?}\",\n 93|        text(&collected)\n 94|    );\n### stream.rs\n 3|//!  - **bounded backpressure**: a flooding child whose output goes unconsumed\n*4|//!    must *stall the reader* (no unbounded buffer); consuming resumes the flow.\n 5|//!  - **value-set coverage under resize** (Spike #5): every injected value is\n 6|//!    delivered even as the surface is resized mid-stream — asserted as a set\n 7|//!    (ConPTY repaint may reorder/duplicate; nothing is lost).\n...\n 9|use std::sync::Arc;\n*10|use std::thread;\n 11|use std::time::Duration;\n 12|\n 13|use spt_term::{PtySession, SessionSurface, SurfaceSize};\n 14|\n*15|/// An echo child (reads stdin, prints each line), OS-appropriate.\n 16|fn echo_child() -> PtySession {\n 17|    #[cfg(unix)]\n 18|    let s = PtySession::spawn_program(\"cat\", Vec::<String>::new(), SurfaceSize::new(24, 80));\n...\n 38|#[test]\n*39|fn bounded_backpressure_stalls_an_unconsumed_reader() {\n 40|    let session = Arc::new(flooder());\n 41|    let stream = session.byte_stream(2).expect(\"open bounded stream\");\n 42|\n 43|    // Let the channel fill (2 chunks) and the next send block.\n*44|    thread::sleep(Duration::from_millis(400));\n*45|    let b1 = stream.drain().bytes_forwarded();\n 46|    // Still not consuming — a bounded buffer cannot keep growing.\n*47|    thread::sleep(Duration::from_millis(400));\n*48|    let b2 = stream.drain().bytes_forwarded();\n 49|    assert_eq!(\n 50|        b1, b2,\n*51|        \"an unconsumed bounded stream must stall the reader (got {b1} -> {b2})\"\n 52|    );\n 53|    assert!(b1 > 0, \"some bytes were buffered before the stall\");\n 54|\n*55|    // Drain a few chunks — the flow must resume past the old plateau.\n 56|    for _ in 0..8 {\n 57|        let _ = stream.recv_timeout(Duration::from_millis(500));\n 58|    }\n*59|    thread::sleep(Duration::from_millis(200));\n*60|    let b3 = stream.drain().bytes_forwarded();\n 61|    assert!(b3 > b2, \"consuming must resume the flow ({b2} -> {b3})\");\n 62|\n 63|    stream.request_stop();\n...\n 70|    let session = echo_child();\n*71|    // Capacity comfortably above the in-flight echoes so single-threaded\n*72|    // inject-then-drain never deadlocks; backpressure is the other test's job.\n 73|    let stream = session.byte_stream(256).expect(\"open stream\");\n 74|\n 75|    const N: usize = 40;\n...\n 112|        .map(|i| format!(\"VAL-{i:03}\"))\n*113|        .filter(|m| !text.contains(m.as_str()))\n 114|        .collect();\n 115|    assert!(\n 116|        have_all,\n### surface.rs\n 10|use std::sync::Arc;\n*11|use std::thread;\n 12|\n 13|use spt_term::{PtySession, SessionSurface, SurfaceSize};\n 14|\n...\n 34|        let s = Arc::clone(&session);\n*35|        handles.push(thread::spawn(move || {\n 36|            for i in 0..150u16 {\n 37|                let rows = 10 + ((t * 7 + i) % 40);\n 38|                let cols = 40 + ((t * 13 + i) % 80);\n...\n 44|    for h in handles {\n*45|        h.join().expect(\"resizer thread must not panic\");\n 46|    }\n 47|\n 48|    // Surface still usable after the storm; tear the child down cleanly.\n...\n 59|fn captures_child_output_off_the_pty() {\n*60|    use std::io::Read;\n 61|\n 62|    let session = PtySession::spawn_program(\n 63|        \"sh\",\n...\n 68|\n*69|    let mut reader = session.take_reader().expect(\"take the master reader\");\n 70|    // Second take is rejected — output has exactly one consumer.\n*71|    assert!(session.take_reader().is_err(), \"reader is once-only\");\n 72|\n 73|    let mut out = Vec::new();\n*74|    reader\n*75|        .read_to_end(&mut out)\n*76|        .expect(\"read child output to EOF\");\n 77|    let text = String::from_utf8_lossy(&out);\n 78|    assert!(\n 79|        text.contains(\"hello-surface\"),\n...\n 88|// Asserted on the unique tempdir leaf (robust against /var↔/private/var and\n*89|// logical-vs-physical pwd differences — the random component is identical\n 90|// either way). Unix-gated like the capture test (clean raw-pipe EOF).\n 91|#[cfg(unix)]\n 92|#[test]\n 93|fn spawn_program_in_lands_the_child_in_the_requested_cwd() {\n*94|    use std::io::Read;\n 95|\n 96|    let dir = tempfile::tempdir().expect(\"tempdir\");\n 97|    let leaf = dir\n...\n 111|\n*112|    let mut reader = session.take_reader().expect(\"take reader\");\n 113|    let mut out = Vec::new();\n*114|    reader.read_to_end(&mut out).expect(\"read child cwd to EOF\");\n 115|    let text = String::from_utf8_lossy(&out);\n 116|    assert!(\n 117|        text.contains(&leaf),\n### winspawn.rs\n 23|        let mut f = std::fs::File::create(&path).expect(\"create temp .cmd\");\n*24|        f.write_all(b\"@echo off\\r\\nexit /b 0\\r\\n\")\n 25|            .expect(\"write temp .cmd\");\n 26|    }\n 27|\n 28|    // `process_id()` is inherent on PtySession; capture it (or the error string)\n*29|    // before dropping the handle — PtySession is not Debug, so format the error.\n 30|    let outcome = PtySession::spawn_program(\n 31|        path.as_os_str(),\n 32|        Vec::<String>::new(),"}],"details":{"scopePath":"crates/spt-term/src, crates/spt-term/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":199,"fileCount":14,"files":["crates/spt-term/src/digest.rs","crates/spt-term/src/lib.rs","crates/spt-term/src/projection.rs","crates/spt-term/src/pty.rs","crates/spt-term/src/reader.rs","crates/spt-term/src/screen.rs","crates/spt-term/src/stream.rs","crates/spt-term/src/surface.rs","crates/spt-term/src/winprog.rs","crates/spt-term/tests/dsr.rs","crates/spt-term/tests/inject.rs","crates/spt-term/tests/stream.rs","crates/spt-term/tests/surface.rs","crates/spt-term/tests/winspawn.rs"],"fileMatches":[{"path":"crates/spt-term/src/digest.rs","count":9},{"path":"crates/spt-term/src/lib.rs","count":6},{"path":"crates/spt-term/src/projection.rs","count":20},{"path":"crates/spt-term/src/pty.rs","count":20},{"path":"crates/spt-term/src/reader.rs","count":20},{"path":"crates/spt-term/src/screen.rs","count":20},{"path":"crates/spt-term/src/stream.rs","count":20},{"path":"crates/spt-term/src/surface.rs","count":10},{"path":"crates/spt-term/src/winprog.rs","count":4},{"path":"crates/spt-term/tests/dsr.rs","count":20},{"path":"crates/spt-term/tests/inject.rs","count":20},{"path":"crates/spt-term/tests/stream.rs","count":15},{"path":"crates/spt-term/tests/surface.rs","count":13},{"path":"crates/spt-term/tests/winspawn.rs","count":2}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-term/\n\n## src/\n### digest.rs\n  57│/// authoritative dedup + cursor key, `(ledger_ordinal << 32) | line_idx` for a\n *58│/// committed entry, blanked to `None` for an open/partial turn — and an optional\n  59│/// `ts`. `Boundary`/`Context` are spt-injected (not transcript records), so they\n  60│/// carry no `seq`, only their `ts`.\n  61│#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n    │...\n  87│    },\n *88│    /// A session boundary marker (REQ-TERM-6): a `/clear` or `/compact` between\n *89│    /// two spanned sessions, rendered as a distinctive divider so a reader sees\n *90│    /// the context reset without losing the thread. `kind` is the trigger\n *91│    /// (`clear` | `compact` | `boot`); `ts` is its RFC3339-UTC instant if known.\n  92│    /// Carried in a preamble turn (input `None`) the spanning projection splices\n  93│    /// between sessions — never produced by the single-session contract fold.\n  94│    Boundary {\n *95│        /// The boundary trigger token (`clear` | `compact` | `boot`).\n  96│        kind: String,\n  97│        /// The boundary's RFC3339-UTC instant, if recorded in the ledger.\n  98│        ts: Option<String>,\n    │...\n 118│    /// contract fold / test paths that have no source ordinal (seq/ts stay `None`;\n*119│    /// the daemon timeline threads them in afterward). [impl->REQ-DIGEST-CURSOR]\n 120│    pub fn agent(text: String) -> Self {\n 121│        DigestEntry::Agent {\n 122│            text,\n    │...\n 148│    /// (REQ-DIGEST-CURSOR). `None` for a preamble turn (no input) or a partial\n*149│    /// turn (its seqs are blanked).\n 150│    #[serde(skip_serializing_if = \"Option::is_none\")]\n 151│    pub input_seq: Option<u64>,\n 152│    /// True for the trailing **open** turn — one no later input has closed. A\n*153│    /// partial turn carries no stable seq (its entries' seqs are blanked); all\n 154│    /// prior input-closed turns are committed. [impl->REQ-DIGEST-CURSOR]\n 155│    #[serde(default, skip_serializing_if = \"std::ops::Not::not\")]\n 156│    pub partial: bool,\n### lib.rs\n 19│//! pipe**; ConPTY is a **screen buffer** (repaint-on-resize reorders + duplicates\n*20│//! the stream; a mid-session attach sees the viewport, not scrollback). The\n 21│//! [`SessionSurface`] trait and the byte-stream / digest contracts therefore must\n 22│//! **not** bake in either OS's semantics — the surface is modeled as a byte stream\n 23│//! with explicit, OS-independent attach/replay semantics.\n   │...\n 30│//   A1  SessionSurface trait + ConPTY/forkpty backends     (REQ-TERM-1)       ← landed\n*31│//   A2  ConPTY-DSR auto-answer drain thread                 (REQ-HAZARD-CONPTY-DSR) ← landed\n 32│//   A3  send-keys / send-line injection                     (REQ-TERM-2)\n 33│//   A4  byte-stream local streaming (bounded backpressure)  (REQ-TERM-3)       ← landed\n 34│//   A5  live activity buffer — digest output types + the log-source projection\n   │...\n 40│pub mod pty;\n*41│pub mod reader;\n 42│pub mod screen;\n 43│pub mod stream;\n 44│pub mod surface;\n   │...\n 50│    record_to_tagged, record_to_tagged_result, window_input_turns, ContextRecord,\n*51│    DigestDiagnostics, DigestRecord, DigestRole, DropReason, DroppedLine, TimelineItem, ToolRef,\n 52│};\n 53│pub use portable_pty::CommandBuilder;\n 54│pub use pty::PtySession;\n*55│pub use reader::Drain;\n 56│// [impl->REQ-BROKER-SCREEN-GRID] the server-side render grid + clean repaint on\n 57│// attach (ADR-0031) — a clean-room `vte::Perform` interpreter of the byte stream.\n 58│pub use screen::ScreenGrid;\n*59│pub use stream::OutputStream;\n 60│pub use surface::{SessionSurface, SurfaceError, SurfaceSize};\n### projection.rs\n   9│//! the semantic turn boundaries in repaint traffic. The split (CONTEXT.md\n *10│//! \"session digest\"): **render surfaces read the PTY** (the raw byte ring,\n *11│//! unchanged); **content surfaces read the logs.** The digest is a rolling\n  12│//! tail-projection of the **same normalized history the echo-commune consumes**\n  13│//! (history subsystem Path A locate+normalize / Path B native store) — one\n  14│//! pipeline, two consumers.\n    │...\n  40│//! **Forward-compat:** unknown object fields are ignored (a future contract field\n *41│//! does not break an older reader); a line that is not a valid record — malformed\n  42│//! JSON, missing/unknown `role`, or a `tool` record without its `tool` object —\n  43│//! is **skipped**, never a panic and never an aborted projection.\n  44│//!\n    │...\n 107│\n*108│/// Why a normalized line was dropped from the projection — the skip-diagnostics\n*109│/// (ADR-0019). The silent `from_str().ok()` is gone: every drop now carries a\n 110│/// reason a `digest-proof` run (or the runtime warn) can surface.\n 111│#[derive(Debug, Clone, PartialEq, Eq)]\n*112│pub enum DropReason {\n 113│    /// Not valid contract JSON: malformed, a non-object, or an unknown/missing\n 114│    /// `role` (serde's message — e.g. \"unknown variant `whisper`\" — is carried).\n 115│    Malformed(String),\n    │...\n 119│\n*120│impl std::fmt::Display for DropReason {\n 121│    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n 122│        match self {\n*123│            DropReason::Malformed(msg) => write!(f, \"not a valid contract record: {msg}\"),\n*124│            DropReason::ToolMissingObject => {\n 125│                write!(f, \"role \\\"tool\\\" without a `tool` object\")\n 126│            }\n 127│        }\n    │...\n 130│\n*131│/// One dropped line, with its 1-based position, raw text, and reason — the unit\n 132│/// of the skip-diagnostics surfaced by `spt adapter digest-proof`.\n 133│#[derive(Debug, Clone, PartialEq, Eq)]\n*134│pub struct DroppedLine {\n 135│    pub line_no: usize,\n 136│    pub raw: String,\n*137│    pub reason: DropReason,\n 138│}\n 139│\n 140│/// Skip-diagnostics for one projection: how many records parsed and every line\n*141│/// that was dropped (with why). The author-time + runtime answer to \"the digest\n 142│/// returns nothing\" (ADR-0019 §2).\n 143│#[derive(Debug, Clone, Default, PartialEq, Eq)]\n 144│pub struct DigestDiagnostics {\n    │...\n 146│    pub parsed: usize,\n*147│    /// Every dropped line, in source order.\n*148│    pub dropped: Vec<DroppedLine>,\n 149│}\n 150│\n 151│impl DigestDiagnostics {\n*152│    /// Number of dropped lines.\n*153│    pub fn drop_count(&self) -> usize {\n*154│        self.dropped.len()\n 155│    }\n*156│    /// The first dropped line (the \"first mismatch\" an author chases).\n*157│    pub fn first_drop(&self) -> Option<&DroppedLine> {\n### pty.rs\n  4│//!\n *5│//! [`PtySession`] spawns a child under a real PTY, owns the master reader and\n  6│//! writer, and serves the [`SessionSurface`] contract. It is **single-surface\n  7│//! per handle** — one child's PTY, not the multiplexed fleet (that supervisor is\n  8│//! the M3b broker).\n   │...\n 11│//!\n*12│//! `resize()` races the reader thread on Windows; the master is therefore held\n 13│//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared\n*14│//! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup\n 15│//! cursor query through the same serialized writer the caller injects input\n 16│//! through — no torn writes.\n 17│\n*18│use std::io::{Read, Write};\n 19│use std::sync::{Arc, Mutex};\n 20│\n 21│use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};\n   │...\n 24│\n*25│/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain\n*26│/// thread so the DSR auto-answer and caller input never interleave mid-write.\n 27│pub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;\n 28│\n 29│/// A child process running under a native PTY, exposed as a [`SessionSurface`].\n   │...\n 31│/// Build one with [`PtySession::spawn`]; drive input via [`SessionSurface::write_input`]\n*32│/// (or the A3 `send_keys`/`send_line` helpers), drain output via\n*33│/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /\n 34│/// [`PtySession::wait`] / [`PtySession::kill`].\n 35│pub struct PtySession {\n*36│    /// The master end, behind a lock so `resize` never races the reader (Spike #5).\n 37│    master: Mutex<Box<dyn MasterPty + Send>>,\n*38│    /// The single PTY writer, shared so the A2 drain thread can answer DSR.\n 39│    writer: SharedWriter,\n 40│    /// The spawned child; behind a lock for `wait`/`try_wait`.\n 41│    child: Mutex<Box<dyn Child + Send + Sync>>,\n 42│    /// A kill handle split from the child so we can terminate from another\n*43│    /// thread even while `wait` blocks (`portable-pty` `ChildKiller`).\n 44│    killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,\n*45│    /// The master reader, taken at most once (`take_reader`).\n*46│    reader: Mutex<Option<Box<dyn Read + Send>>>,\n 47│    /// The child OS pid, captured at spawn.\n 48│    pid: Option<u32>,\n 49│}\n   │...\n 53│    ///\n*54│    /// The slave end is dropped immediately after the child is spawned: the\n 55│    /// child holds the only remaining slave handle, which lets EOF propagate on\n 56│    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF\n*57│    /// behaviour is governed by the writer, handled by the A2 drain discipline.)\n 58│    //\n 59│    // [impl->REQ-TERM-1]\n 60│    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {\n   │...\n 69│            .map_err(|e| SurfaceError::Spawn(e.to_string()))?;\n*70│        // Drop the slave so the child owns the only slave-side handle.\n*71│        drop(pair.slave);\n 72│\n*73│        let reader = pair\n 74│            .master\n*75│            .try_clone_reader()\n 76│            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n 77│        let writer = pair\n 78│            .master\n   │...\n 88│            killer: Mutex::new(killer),\n*89│            reader: Mutex::new(Some(reader)),\n 90│            pid,\n 91│        })\n 92│    }\n### reader.rs\n  *1│//! Output drain pump — the ConPTY-DSR auto-answer reader (M3a A2).\n   2│//!\n   3│//! ConPTY can emit a cursor-position query `ESC [ 6 n` and **withhold all child\n   4│//! stdout until the terminal answers it** (Spike #1: a run captured exactly the\n    │...\n  15│//! **No-EOF-while-writer-held (Spike #1 secondary gotcha):** a ConPTY master\n *16│//! does *not* EOF while the writer is held, so the read loop runs on its own\n *17│//! thread and shutdown **never gates on a blocking `read()` returning**. The\n  18│//! broker stops a surface by terminating the child (which tears the PTY down and\n *19│//! unblocks the read); the [`Drain`] stop flag is a cooperative secondary signal\n *20│//! observed whenever a read does return. Deterministic, ordered teardown (drop\n  21│//! the writer, then the master) is the M3b broker's lifecycle responsibility.\n  22│\n *23│use std::io::{Read, Write};\n  24│use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\n  25│use std::sync::Arc;\n *26│use std::thread;\n  27│\n  28│use crate::pty::{PtySession, SharedWriter};\n  29│use crate::surface::SurfaceError;\n    │...\n  38│/// Streaming matcher for [`DSR_QUERY`] that survives chunk boundaries (the 4-byte\n *39│/// query can split across two `read()`s). `DSR_QUERY` has no proper prefix that\n  40│/// is also a suffix, so a single \"bytes matched so far\" counter is sufficient.\n  41│#[derive(Default)]\n  42│struct DsrMatcher {\n    │...\n  65│\n *66│/// A running drain pump over a surface's output. Holds live counters and a\n  67│/// cooperative stop flag; see the module docs for the teardown contract.\n *68│pub struct Drain {\n  69│    stop: Arc<AtomicBool>,\n  70│    dsr_answered: Arc<AtomicU64>,\n  71│    bytes: Arc<AtomicU64>,\n *72│    handle: Option<thread::JoinHandle<()>>,\n  73│}\n  74│\n *75│impl Drain {\n  76│    /// Total DSR queries answered so far (live).\n  77│    pub fn dsr_answered(&self) -> u64 {\n  78│        self.dsr_answered.load(Ordering::Relaxed)\n    │...\n  85│\n *86│    /// Ask the pump to stop. Observed the next time a `read()` returns; it does\n *87│    /// **not** interrupt a read already blocked (that requires tearing the child\n  88│    /// down — the no-EOF-while-writer-held discipline).\n  89│    pub fn request_stop(&self) {\n  90│        self.stop.store(true, Ordering::Relaxed);\n    │...\n  92│\n *93│    /// Join the pump thread. Only returns once the underlying `read()` has\n *94│    /// unblocked (EOF / error / a returned read that observes the stop flag) —\n  95│    /// on ConPTY that means the child must be torn down first. Best-effort.\n  96│    pub fn join(mut self) {\n  97│        if let Some(h) = self.handle.take() {\n    │...\n 102│\n*103│impl Drain {\n*104│    /// Spawn the drain thread over `reader`, answering DSR via `writer` and\n 105│    /// forwarding every byte to `sink`. The `sink` may block (that is how A4's\n 106│    /// bounded channel applies backpressure).\n*107│    fn spawn<R, S>(reader: R, writer: SharedWriter, mut sink: S) -> Self\n 108│    where\n*109│        R: Read + Send + 'static,\n 110│        S: FnMut(&[u8]) + Send + 'static,\n 111│    {\n 112│        let stop = Arc::new(AtomicBool::new(false));\n### screen.rs\n  11│//!\n *12│//! **Scope (ratified, ADR-0031):** current screen only — the visible viewport\n  13│//! (cell buffer + cursor + main/alt buffer + the modes a repaint needs). Scrollback\n  14│//! stays the raw ring (history). The grid drives ONLY the attach repaint; live\n  15│//! frames after attach are the unchanged raw bytes.\n    │...\n 141│impl Cell {\n*142│    /// A cell that renders as nothing on a freshly-cleared screen: a space with the\n*143│    /// default pen. Trailing runs of these are trimmed from a repaint.\n*144│    fn is_default_blank(&self) -> bool {\n 145│        self.ch == ' ' && self.pen == Pen::default()\n 146│    }\n 147│}\n    │...\n 149│/// The public screen grid: a `vte` parser paired with the clean-room [`GridState`]\n*150│/// it drives. Single-writer by construction (the broker's session drain thread) —\n 151│/// mirrors the existing single-producer ring (ADR-0031, hazard 7.12).\n 152│pub struct ScreenGrid {\n 153│    parser: Parser,\n    │...\n 157│impl ScreenGrid {\n*158│    /// A fresh grid sized `rows × cols` (a blank main screen, cursor home).\n 159│    pub fn new(rows: u16, cols: u16) -> Self {\n 160│        ScreenGrid {\n 161│            parser: Parser::new(),\n    │...\n 166│    /// Feed a raw output chunk through the parser, advancing the current screen.\n*167│    /// Called on the session drain path AFTER the ring append + fan-out, so it adds\n 168│    /// no latency to controller/viewer delivery (ADR-0031, REQ-HAZARD-INJECT-CONTROL-COEXIST).\n 169│    pub fn advance(&mut self, bytes: &[u8]) {\n 170│        for &b in bytes {\n    │...\n 183│    /// Synthesize a clean repaint of the CURRENT screen: a byte sequence that,\n*184│    /// applied to a fresh terminal, reproduces the visible viewport (alt/main\n 185│    /// buffer, cells with minimal-ish SGR runs, cursor position + visibility) with\n 186│    /// NO raw scrollback and NO mid-stream rendering debris. This replaces the\n 187│    /// raw-ring initial batch at `become_controller` / `add_viewer`.\n    │...\n 211│    /// cold attach carries the CURRENT title, not the attacher's stale one (E-2).\n*212│    /// Cleared by RIS (full_reset rebuilds the state); survives resize.\n 213│    // [impl->REQ-BROKER-SCREEN-GRID]\n 214│    title: Option<String>,\n 215│}\n    │...\n 223│            cols,\n*224│            main: blank_buffer(rows, cols),\n*225│            alt: blank_buffer(rows, cols),\n 226│            alt_active: false,\n 227│            row: 0,\n 228│            col: 0,\n    │...\n 254│\n*255│    /// The pen an erase fills with: attributes cleared but the current background\n*256│    /// preserved (so a status-bar erase keeps its colour).\n*257│    fn erase_pen(&self) -> Pen {\n 258│        Pen {\n 259│            bg: self.pen.bg,\n 260│            ..Pen::default()\n    │...\n 263│\n*264│    fn blank(&self) -> Cell {\n 265│        Cell {\n 266│            ch: ' ',\n*267│            pen: self.erase_pen(),\n 268│        }\n 269│    }\n 270│\n    │...\n 299│    fn scroll_up(&mut self, n: usize) {\n*300│        let blank = self.blank();\n 301│        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n 302│        let cols = self.cols;\n 303│        let buf = self.buf_mut();\n    │...\n 306│                buf[top..=bottom].rotate_left(1);\n*307│                buf[bottom] = vec![blank; cols];\n 308│            }\n 309│        }\n 310│    }\n    │...\n 312│    fn scroll_down(&mut self, n: usize) {\n*313│        let blank = self.blank();\n 314│        let (top, bottom) = (self.scroll_top, self.scroll_bottom);\n 315│        let cols = self.cols;\n 316│        let buf = self.buf_mut();\n    │...\n 319│                buf[top..=bottom].rotate_right(1);\n*320│                buf[top] = vec![blank; cols];\n 321│            }\n 322│        }\n 323│    }\n### stream.rs\n  2│//!\n *3│//! [`OutputStream`] is the v1 consumer shape: the A2 drain forwards every byte\n  4│//! into a bounded `sync_channel`, and a consumer pulls chunks off the other end.\n *5│//! When the consumer falls behind, the channel fills, the drain's `send` blocks,\n *6│//! the reader stops draining, the PTY buffer fills, and the child blocks on\n  7│//! write — **end-to-end bounded backpressure, no unbounded buffer**. The off-node\n  8│//! hop (M4 transport) and the predictive \"feels-local\" layer ride on top of this\n  9│//! substrate later; M3a delivers the local shape only.\n   │...\n 11│//! **OS-neutral (Spike #4 / #5):** the stream is bytes, nothing more. It makes no\n*12│//! ordering promise beyond \"every byte the reader forwarded, in the order it was\n 13│//! forwarded\" — on ConPTY a repaint-on-resize legitimately reorders/duplicates\n 14│//! content, so consumers (e.g. the A5 digest) match on *value-set coverage*, not\n 15│//! byte-exact sequence.\n   │...\n 20│use crate::pty::PtySession;\n*21│use crate::reader::Drain;\n 22│use crate::surface::SurfaceError;\n 23│\n*24│/// A bounded stream of the child's output chunks. Dropping it unblocks a drain\n 25│/// that is parked on a full channel (the blocked `send` returns once the\n 26│/// receiver is gone), letting the pump wind down once the child is torn down.\n*27│pub struct OutputStream {\n 28│    rx: Receiver<Vec<u8>>,\n*29│    drain: Drain,\n 30│}\n 31│\n*32│impl OutputStream {\n 33│    /// Block for the next output chunk. Returns `None` once the stream has ended\n*34│    /// (the drain thread exited and dropped its sender — i.e. the child's output\n 35│    /// reached EOF or the pump was stopped).\n 36│    pub fn recv(&self) -> Option<Vec<u8>> {\n 37│        self.rx.recv().ok()\n   │...\n 39│\n*40│    /// Non-blocking poll for the next chunk, if one is already buffered.\n 41│    pub fn try_recv(&self) -> Option<Vec<u8>> {\n 42│        self.rx.try_recv().ok()\n 43│    }\n   │...\n 49│\n*50│    /// The underlying drain — live `bytes_forwarded` / `dsr_answered` counters\n 51│    /// and the cooperative stop flag.\n*52│    pub fn drain(&self) -> &Drain {\n*53│        &self.drain\n 54│    }\n 55│\n*56│    /// Ask the pump to stop (cooperative — see [`Drain::request_stop`]).\n 57│    pub fn request_stop(&self) {\n*58│        self.drain.request_stop();\n 59│    }\n 60│}\n 61│\n   │...\n 66│    ///\n*67│    /// Takes the master reader (once). The returned [`OutputStream`] is the sole\n 68│    /// consumer; chunks the consumer never takes apply backpressure rather than\n 69│    /// growing an unbounded buffer.\n 70│    //\n 71│    // [impl->REQ-TERM-3]\n*72│    pub fn byte_stream(&self, capacity: usize) -> Result<OutputStream, SurfaceError> {\n 73│        let (tx, rx) = sync_channel::<Vec<u8>>(capacity);\n*74│        let drain = self.drain(move |chunk| {\n 75│            // Bounded send: blocks when `capacity` chunks are unconsumed\n*76│            // (backpressure); drops silently once the receiver is gone so a\n 77│            // torn-down consumer never wedges the pump.\n 78│            let _ = tx.send(chunk.to_vec());\n 79│        })?;\n### surface.rs\n   2│//!\n  *3│//! A [`SessionSurface`] is \"anything you can write input to and read output\n   4│//! from\" — the abstraction every hosted agent session is driven through. The\n   5│//! native PTY backend ([`crate::pty::PtySession`]) is the M3a implementation;\n   6│//! M4's network-attached surface and the GUI text pane slot in behind the same\n    │...\n  14│//! is a separate, equally OS-neutral concern (A4, [`crate::pty::PtySession`]'s\n *15│//! reader). No backend's stream semantics leak through this contract.\n  16│\n  17│use portable_pty::PtySize;\n  18│\n    │...\n  70│    Spawn(String),\n *71│    /// An I/O operation on the surface failed (read/write/resize).\n  72│    Io(std::io::Error),\n *73│    /// The single writer handle was already taken (PTY writers are once-only).\n  74│    WriterTaken,\n *75│    /// The single reader handle was already taken.\n *76│    ReaderTaken,\n  77│}\n  78│\n  79│impl std::fmt::Display for SurfaceError {\n    │...\n  84│            SurfaceError::Io(e) => write!(f, \"surface i/o failed: {e}\"),\n *85│            SurfaceError::WriterTaken => write!(f, \"the surface writer was already taken\"),\n *86│            SurfaceError::ReaderTaken => write!(f, \"the surface reader was already taken\"),\n  87│        }\n  88│    }\n  89│}\n    │...\n  96│/// `Send + Sync` so the M3b broker can host the surface behind a shared handle\n *97│/// and drive `write_input` / `resize` from its IPC thread while a separate\n *98│/// reader thread drains output.\n  99│//\n 100│// [impl->REQ-TERM-1]\n 101│pub trait SessionSurface: Send + Sync {\n### winprog.rs\n  90│    seg.rfind('.')\n *91│        .filter(|&i| i > 0)\n  92│        .map(|i| seg[i + 1..].to_ascii_lowercase())\n  93│}\n  94│\n    │...\n 117│/// - An explicit extension ⇒ classify by it (no search).\n*118│/// - A bare name ⇒ search every `dir` × `exts` (PATHEXT order already prefers\n 119│///   `.EXE` over `.CMD`), then an extensionless fallback, then passthrough.\n 120│/// - A separator-bearing extensionless path ⇒ search its own dir × `exts`, then\n 121│///   the literal path, then passthrough.\n    │...\n 172│        .split(';')\n*173│        .map(|s| s.trim())\n*174│        .filter(|s| !s.is_empty())\n 175│        .map(|s| {\n 176│            if s.starts_with('.') {\n 177│                s.to_string()\n\n## tests/\n### dsr.rs\n *1│//! A2 regression tests for the ConPTY-DSR auto-answer drain pump.\n  2│//!\n  3│//! Whether the host ConPTY emits the DSR query is build/program-dependent, so\n  4│//! the *auto-answer mechanism* is proven deterministically with a synthetic\n *5│//! query in the `reader` unit tests. These integration tests prove the\n *6│//! complementary end-to-end guarantee on a **real** PTY: the drain forwards the\n  7│//! child's stdout and nothing is withheld (on any ConPTY that does stall, the\n  8│//! auto-answer is what keeps this passing).\n  9│\n 10│use std::sync::{Arc, Mutex};\n*11│use std::thread;\n 12│use std::time::Duration;\n 13│\n 14│use spt_term::{PtySession, SurfaceSize};\n   │...\n 38│        }\n*39│        thread::sleep(Duration::from_millis(10));\n 40│    }\n 41│    false\n 42│}\n   │...\n 45│#[test]\n*46│fn drain_forwards_child_output() {\n*47│    let session = print_marker(\"HELLO-DRAIN\");\n 48│    let collected = Arc::new(Mutex::new(Vec::new()));\n 49│    let sink = Arc::clone(&collected);\n*50│    let drain = session\n*51│        .drain(move |chunk| sink.lock().unwrap().extend_from_slice(chunk))\n*52│        .expect(\"start drain\");\n 53│\n*54│    let found = wait_for(&collected, \"HELLO-DRAIN\");\n*55│    drain.request_stop();\n 56│    let _ = session.kill();\n 57│\n 58│    assert!(\n 59│        found,\n*60│        \"the drain must forward the child's stdout (ConPTY: only after DSR is answered)\"\n 61│    );\n*62│    // The reader is once-only: drain took it.\n*63│    assert!(session.take_reader().is_err(), \"drain owns the reader\");\n 64│}\n 65│\n 66│// [int->REQ-HAZARD-CONPTY-DSR]\n   │...\n 68│#[test]\n*69│fn conpty_output_not_withheld_under_the_drain() {\n 70│    let session = print_marker(\"DSR-PROOF\");\n 71│    let collected = Arc::new(Mutex::new(Vec::new()));\n 72│    let sink = Arc::clone(&collected);\n*73│    let drain = session\n*74│        .drain(move |chunk| sink.lock().unwrap().extend_from_slice(chunk))\n*75│        .expect(\"start drain\");\n 76│\n*77│    // The drain runs the DSR auto-answer; ConPTY stdout reaches us either way.\n 78│    let found = wait_for(&collected, \"DSR-PROOF\");\n### inject.rs\n  2│//!\n *3│//! An echo child (`cat` on Unix, `findstr \".\"` on Windows) reads stdin and\n  4│//! prints each line back, so injected input round-trips through the real PTY and\n *5│//! into the drained output stream. Ordering is asserted by index; `Ctrl-C`\n  6│//! (`0x03`) must interrupt the child.\n  7│\n  8│use std::sync::{Arc, Mutex};\n *9│use std::thread;\n 10│use std::time::Duration;\n 11│\n*12│use spt_term::{Drain, PtySession, SessionSurface, SurfaceSize};\n 13│\n*14│/// An echo child: reads stdin, writes each line to stdout, OS-appropriate.\n 15│fn echo_child() -> PtySession {\n 16│    #[cfg(unix)]\n 17│    let s = PtySession::spawn_program(\"cat\", Vec::<String>::new(), SurfaceSize::new(24, 80));\n   │...\n 22│\n*23│fn drain_into(session: &PtySession) -> (Drain, Arc<Mutex<Vec<u8>>>) {\n 24│    let collected = Arc::new(Mutex::new(Vec::new()));\n 25│    let sink = Arc::clone(&collected);\n*26│    let drain = session\n*27│        .drain(move |chunk| sink.lock().unwrap().extend_from_slice(chunk))\n*28│        .expect(\"start drain\");\n*29│    (drain, collected)\n 30│}\n 31│\n 32│fn text(collected: &Arc<Mutex<Vec<u8>>>) -> String {\n   │...\n 40│        }\n*41│        thread::sleep(Duration::from_millis(10));\n 42│    }\n 43│    false\n 44│}\n   │...\n 49│    let session = echo_child();\n*50│    let (drain, collected) = drain_into(&session);\n 51│\n 52│    session.send_line(\"INJECT-ALPHA\").expect(\"send first line\");\n 53│    session.send_line(\"INJECT-BRAVO\").expect(\"send second line\");\n   │...\n 60│    let _ = session.send_keys(&[0x03]);\n*61│    drain.request_stop();\n 62│    let _ = session.kill();\n 63│\n 64│    let t = text(&collected);\n   │...\n 80│    let session = echo_child();\n*81│    let (drain, collected) = drain_into(&session);\n 82│\n 83│    // Handshake: inject a line and wait for it to echo back. This proves the\n*84│    // child is up and *reading* before we deliver Ctrl-C — otherwise the signal\n*85│    // races child startup and can be dropped before its handler is installed.\n 86│    session\n*87│        .send_line(\"READY-PROBE\")\n*88│        .expect(\"send readiness probe\");\n*89│    let ready = wait_until(&collected, |t| t.contains(\"READY-PROBE\"));\n 90│    assert!(\n*91│        ready,\n 92│        \"echo child must come up and echo before Ctrl-C: {:?}\",\n 93│        text(&collected)\n 94│    );\n### stream.rs\n   3│//!  - **bounded backpressure**: a flooding child whose output goes unconsumed\n  *4│//!    must *stall the reader* (no unbounded buffer); consuming resumes the flow.\n   5│//!  - **value-set coverage under resize** (Spike #5): every injected value is\n   6│//!    delivered even as the surface is resized mid-stream — asserted as a set\n   7│//!    (ConPTY repaint may reorder/duplicate; nothing is lost).\n    │...\n   9│use std::sync::Arc;\n *10│use std::thread;\n  11│use std::time::Duration;\n  12│\n  13│use spt_term::{PtySession, SessionSurface, SurfaceSize};\n  14│\n *15│/// An echo child (reads stdin, prints each line), OS-appropriate.\n  16│fn echo_child() -> PtySession {\n  17│    #[cfg(unix)]\n  18│    let s = PtySession::spawn_program(\"cat\", Vec::<String>::new(), SurfaceSize::new(24, 80));\n    │...\n  38│#[test]\n *39│fn bounded_backpressure_stalls_an_unconsumed_reader() {\n  40│    let session = Arc::new(flooder());\n  41│    let stream = session.byte_stream(2).expect(\"open bounded stream\");\n  42│\n  43│    // Let the channel fill (2 chunks) and the next send block.\n *44│    thread::sleep(Duration::from_millis(400));\n *45│    let b1 = stream.drain().bytes_forwarded();\n  46│    // Still not consuming — a bounded buffer cannot keep growing.\n *47│    thread::sleep(Duration::from_millis(400));\n *48│    let b2 = stream.drain().bytes_forwarded();\n  49│    assert_eq!(\n  50│        b1, b2,\n *51│        \"an unconsumed bounded stream must stall the reader (got {b1} -> {b2})\"\n  52│    );\n  53│    assert!(b1 > 0, \"some bytes were buffered before the stall\");\n  54│\n *55│    // Drain a few chunks — the flow must resume past the old plateau.\n  56│    for _ in 0..8 {\n  57│        let _ = stream.recv_timeout(Duration::from_millis(500));\n  58│    }\n *59│    thread::sleep(Duration::from_millis(200));\n *60│    let b3 = stream.drain().bytes_forwarded();\n  61│    assert!(b3 > b2, \"consuming must resume the flow ({b2} -> {b3})\");\n  62│\n  63│    stream.request_stop();\n    │...\n  70│    let session = echo_child();\n *71│    // Capacity comfortably above the in-flight echoes so single-threaded\n *72│    // inject-then-drain never deadlocks; backpressure is the other test's job.\n  73│    let stream = session.byte_stream(256).expect(\"open stream\");\n  74│\n  75│    const N: usize = 40;\n    │...\n 112│        .map(|i| format!(\"VAL-{i:03}\"))\n*113│        .filter(|m| !text.contains(m.as_str()))\n 114│        .collect();\n 115│    assert!(\n 116│        have_all,\n### surface.rs\n  10│use std::sync::Arc;\n *11│use std::thread;\n  12│\n  13│use spt_term::{PtySession, SessionSurface, SurfaceSize};\n  14│\n    │...\n  34│        let s = Arc::clone(&session);\n *35│        handles.push(thread::spawn(move || {\n  36│            for i in 0..150u16 {\n  37│                let rows = 10 + ((t * 7 + i) % 40);\n  38│                let cols = 40 + ((t * 13 + i) % 80);\n    │...\n  44│    for h in handles {\n *45│        h.join().expect(\"resizer thread must not panic\");\n  46│    }\n  47│\n  48│    // Surface still usable after the storm; tear the child down cleanly.\n    │...\n  59│fn captures_child_output_off_the_pty() {\n *60│    use std::io::Read;\n  61│\n  62│    let session = PtySession::spawn_program(\n  63│        \"sh\",\n    │...\n  68│\n *69│    let mut reader = session.take_reader().expect(\"take the master reader\");\n  70│    // Second take is rejected — output has exactly one consumer.\n *71│    assert!(session.take_reader().is_err(), \"reader is once-only\");\n  72│\n  73│    let mut out = Vec::new();\n *74│    reader\n *75│        .read_to_end(&mut out)\n *76│        .expect(\"read child output to EOF\");\n  77│    let text = String::from_utf8_lossy(&out);\n  78│    assert!(\n  79│        text.contains(\"hello-surface\"),\n    │...\n  88│// Asserted on the unique tempdir leaf (robust against /var↔/private/var and\n *89│// logical-vs-physical pwd differences — the random component is identical\n  90│// either way). Unix-gated like the capture test (clean raw-pipe EOF).\n  91│#[cfg(unix)]\n  92│#[test]\n  93│fn spawn_program_in_lands_the_child_in_the_requested_cwd() {\n *94│    use std::io::Read;\n  95│\n  96│    let dir = tempfile::tempdir().expect(\"tempdir\");\n  97│    let leaf = dir\n    │...\n 111│\n*112│    let mut reader = session.take_reader().expect(\"take reader\");\n 113│    let mut out = Vec::new();\n*114│    reader.read_to_end(&mut out).expect(\"read child cwd to EOF\");\n 115│    let text = String::from_utf8_lossy(&out);\n 116│    assert!(\n 117│        text.contains(&leaf),\n### winspawn.rs\n 23│        let mut f = std::fs::File::create(&path).expect(\"create temp .cmd\");\n*24│        f.write_all(b\"@echo off\\r\\nexit /b 0\\r\\n\")\n 25│            .expect(\"write temp .cmd\");\n 26│    }\n 27│\n 28│    // `process_id()` is inherent on PtySession; capture it (or the error string)\n*29│    // before dropping the handle — PtySession is not Debug, so format the error.\n 30│    let outcome = PtySession::spawn_program(\n 31│        path.as_os_str(),\n 32│        Vec::<String>::new(),"},"isError":false,"timestamp":1784365520665}}
{"type":"message","id":"4f1fecdc","parentId":"8c5569c2","timestamp":"2026-07-18T09:05:20.695Z","message":{"role":"toolResult","toolCallId":"call_mJRWSlg5c3tpbN7jH47zMHjJ|fc_0ce5441d29b9f05d016a5b41d06020819b97bb8f36893dab02","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## broker.rs\n 6083|    /// the ADR-0031 screen grid, a controller's initial batch is a SINGLE\n*6084|    /// synthesized clean repaint at pseudo-seq `next_seq - 1` (not the raw ring), so\n*6085|    /// the reorder hazard is now: a SUPERSEDED writer flushing its STALE repaint\n*6086|    /// ahead of the surviving writer's current one. We make the two repaints carry\n 6087|    /// DISTINCT seqs so a leak is order-detectable:\n 6088|    ///   - append seqs 0,1 → grid has content, `next_seq = 2`.\n*6089|    ///   - `become_controller(A, 0)` → writer-A initial = [repaint@1].\n 6090|    ///   - append seq 2 (grid advances) → `next_seq = 3`.\n*6091|    ///   - `become_controller(B, 0)` → writer-B initial = [repaint@2]; this bumps\n 6092|    ///     `controller_epoch`, SUPERSEDING writer-A.\n 6093|    ///\n 6094|    /// Post-fix the second `become_controller` bumps the shared epoch BEFORE\n 6095|    /// spawning writer-B, so writer-A's in-lock gate makes it bail before committing\n*6096|    /// any frame. The wire shows exactly `[2]` — the current-screen repaint — never\n*6097|    /// a stale `[1, ..]` (writer-A's superseded repaint winning the socket first).\n 6098|    ///\n 6099|    /// DETERMINISM: the test holds `send.lock()` across BOTH `become_controller`\n 6100|    /// calls (and the interleaved append), so neither writer thread can acquire the\n...\n 6105|    /// RED-on-revert / non-vacuity: writer-A is genuinely poised with a non-empty\n*6106|    /// initial batch [repaint@1]; ONLY the in-lock gate stops it. Neuter the\n 6107|    /// `if controller_epoch.load(Acquire) != epoch { return; }` inside\n 6108|    /// `controller_writer`'s `send.lock()` arm and writer-A flushes its stale\n*6109|    /// repaint@1; since both writers serialize on the SAME socket mutex, writer-A's\n 6110|    /// seq 1 can win the first slot → the first frame read is `1`, and\n 6111|    /// `assert_eq!(seqs, [2])` fails — exactly the `got stale seq` reorder this\n 6112|    /// hazard is about.\n...\n 6134|\n*6135|        // writer-A: registers at next_seq-1 = 1 → initial = [repaint@1].\n 6136|        log.become_controller(Arc::clone(&send), None, 0, 0);\n 6137|        // A further chunk advances the grid to a NEW current screen (seq 2). The\n 6138|        // handoff to A's (pinned) channel is buffered; A never gets to write it.\n...\n 6142|        );\n*6143|        // writer-B: registers at next_seq-1 = 2 → initial = [repaint@2]. Bumps the\n 6144|        // epoch; writer-A is superseded. Both writers park on the pinned socket.\n 6145|        log.become_controller(Arc::clone(&send), None, 0, 0);\n 6146|\n 6147|        // Release the socket: the writers race for the mutex. The in-lock gate makes\n*6148|        // superseded writer-A bail; only writer-B's current-screen repaint@2 lands.\n 6149|        drop(guard);\n 6150|\n 6151|        let seqs = drain_output_seqs(&mut client, 1);\n...\n 6154|            vec![2],\n*6155|            \"only the surviving writer-B's CURRENT-screen repaint (seq 2) reaches the \\\n*6156|             wire — a superseded writer-A flushing its stale repaint@1 would put seq \\\n 6157|             1 first (the reorder this hazard guards)\"\n 6158|        );\n 6159|\n...\n 6169|\n*6170|        log.clear_controller();\n 6171|        drop(send);\n 6172|        drop(log);\n 6173|    }\n...\n 6177|    /// SAME Request re-served after a dispatcher restart) or newer. A REPLAYED\n*6178|    /// stale Request — an UNFINISHED backlog row whose viewport died raw, the\n*6179|    /// shape finished-row retirement cannot see — carries an OLDER generation\n 6180|    /// than the live replacement controller and must be REFUSED busy, never\n 6181|    /// silently steal the slot. `0` on either side keeps the N-1/local legacy\n 6182|    /// re-take (a legacy peer must not be locked out).\n...\n 6239|\n*6240|        log.clear_controller();\n 6241|    }\n 6242|\n 6243|    /// The gap-resume re-subscribe (gate round 1 must-fix 1): a serve worker's\n...\n 6248|    /// (b) the worker's terminal `detach_session_gen(Some(serve_gen))` still\n*6249|    /// matches and clears its own slot. RED-on-revert: the pre-fix helper\n 6250|    /// re-subscribed with gen=0 — the resume reset `attach_gen` to 0, the\n 6251|    /// stale gen-100 replay then hit the legacy `incumbent_gen == 0` arm and\n 6252|    /// silently re-took the slot (the steal re-armed), and the gen-200\n...\n 6301|\n*6302|        // (b) The worker's terminal generation-validated release still clears.\n 6303|        log.detach_if_gen(&worker, Some(200));\n 6304|        assert!(\n 6305|            log.controller.is_none(),"}],"details":{"scopePath":"crates/spt-daemon/src/broker.rs, crates/spt-term/src/screen.rs, crates/spt-daemon/tests, crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":20,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## broker.rs\n 6083│    /// the ADR-0031 screen grid, a controller's initial batch is a SINGLE\n*6084│    /// synthesized clean repaint at pseudo-seq `next_seq - 1` (not the raw ring), so\n*6085│    /// the reorder hazard is now: a SUPERSEDED writer flushing its STALE repaint\n*6086│    /// ahead of the surviving writer's current one. We make the two repaints carry\n 6087│    /// DISTINCT seqs so a leak is order-detectable:\n 6088│    ///   - append seqs 0,1 → grid has content, `next_seq = 2`.\n*6089│    ///   - `become_controller(A, 0)` → writer-A initial = [repaint@1].\n 6090│    ///   - append seq 2 (grid advances) → `next_seq = 3`.\n*6091│    ///   - `become_controller(B, 0)` → writer-B initial = [repaint@2]; this bumps\n 6092│    ///     `controller_epoch`, SUPERSEDING writer-A.\n 6093│    ///\n 6094│    /// Post-fix the second `become_controller` bumps the shared epoch BEFORE\n 6095│    /// spawning writer-B, so writer-A's in-lock gate makes it bail before committing\n*6096│    /// any frame. The wire shows exactly `[2]` — the current-screen repaint — never\n*6097│    /// a stale `[1, ..]` (writer-A's superseded repaint winning the socket first).\n 6098│    ///\n 6099│    /// DETERMINISM: the test holds `send.lock()` across BOTH `become_controller`\n 6100│    /// calls (and the interleaved append), so neither writer thread can acquire the\n     │...\n 6105│    /// RED-on-revert / non-vacuity: writer-A is genuinely poised with a non-empty\n*6106│    /// initial batch [repaint@1]; ONLY the in-lock gate stops it. Neuter the\n 6107│    /// `if controller_epoch.load(Acquire) != epoch { return; }` inside\n 6108│    /// `controller_writer`'s `send.lock()` arm and writer-A flushes its stale\n*6109│    /// repaint@1; since both writers serialize on the SAME socket mutex, writer-A's\n 6110│    /// seq 1 can win the first slot → the first frame read is `1`, and\n 6111│    /// `assert_eq!(seqs, [2])` fails — exactly the `got stale seq` reorder this\n 6112│    /// hazard is about.\n     │...\n 6134│\n*6135│        // writer-A: registers at next_seq-1 = 1 → initial = [repaint@1].\n 6136│        log.become_controller(Arc::clone(&send), None, 0, 0);\n 6137│        // A further chunk advances the grid to a NEW current screen (seq 2). The\n 6138│        // handoff to A's (pinned) channel is buffered; A never gets to write it.\n     │...\n 6142│        );\n*6143│        // writer-B: registers at next_seq-1 = 2 → initial = [repaint@2]. Bumps the\n 6144│        // epoch; writer-A is superseded. Both writers park on the pinned socket.\n 6145│        log.become_controller(Arc::clone(&send), None, 0, 0);\n 6146│\n 6147│        // Release the socket: the writers race for the mutex. The in-lock gate makes\n*6148│        // superseded writer-A bail; only writer-B's current-screen repaint@2 lands.\n 6149│        drop(guard);\n 6150│\n 6151│        let seqs = drain_output_seqs(&mut client, 1);\n     │...\n 6154│            vec![2],\n*6155│            \"only the surviving writer-B's CURRENT-screen repaint (seq 2) reaches the \\\n*6156│             wire — a superseded writer-A flushing its stale repaint@1 would put seq \\\n 6157│             1 first (the reorder this hazard guards)\"\n 6158│        );\n 6159│\n     │...\n 6169│\n*6170│        log.clear_controller();\n 6171│        drop(send);\n 6172│        drop(log);\n 6173│    }\n     │...\n 6177│    /// SAME Request re-served after a dispatcher restart) or newer. A REPLAYED\n*6178│    /// stale Request — an UNFINISHED backlog row whose viewport died raw, the\n*6179│    /// shape finished-row retirement cannot see — carries an OLDER generation\n 6180│    /// than the live replacement controller and must be REFUSED busy, never\n 6181│    /// silently steal the slot. `0` on either side keeps the N-1/local legacy\n 6182│    /// re-take (a legacy peer must not be locked out).\n     │...\n 6239│\n*6240│        log.clear_controller();\n 6241│    }\n 6242│\n 6243│    /// The gap-resume re-subscribe (gate round 1 must-fix 1): a serve worker's\n     │...\n 6248│    /// (b) the worker's terminal `detach_session_gen(Some(serve_gen))` still\n*6249│    /// matches and clears its own slot. RED-on-revert: the pre-fix helper\n 6250│    /// re-subscribed with gen=0 — the resume reset `attach_gen` to 0, the\n 6251│    /// stale gen-100 replay then hit the legacy `incumbent_gen == 0` arm and\n 6252│    /// silently re-took the slot (the steal re-armed), and the gen-200\n     │...\n 6301│\n*6302│        // (b) The worker's terminal generation-validated release still clears.\n 6303│        log.detach_if_gen(&worker, Some(200));\n 6304│        assert!(\n 6305│            log.controller.is_none(),"},"isError":false,"timestamp":1784365520695}}
{"type":"message","id":"81b1ada3","parentId":"4f1fecdc","timestamp":"2026-07-18T09:05:20.695Z","message":{"role":"toolResult","toolCallId":"call_MXrX6rulaYS3WBT720C07POH|fc_0ce5441d29b9f05d016a5b41d0600c819ba216b10e7ef210a7","toolName":"read","content":[{"type":"text","text":"//! Native-PTY session backend — ConPTY on Windows 10+, `forkpty(3)` on Unix,\n//! both via `portable-pty` (the exact crate + handoff shape Spikes #1 / #4\n//! proved).\n//!\n//! [`PtySession`] spawns a child under a real PTY, owns the master reader and\n//! writer, and serves the [`SessionSurface`] contract. It is **single-surface\n//! per handle** — one child's PTY, not the multiplexed fleet (that supervisor is\n//! the M3b broker).\n//!\n//! ## Concurrency discipline (Spike #5, binding)\n//!\n//! `resize()` races the reader thread on Windows; the master is therefore held\n//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared\n//! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup\n//! cursor query through the same serialized writer the caller injects input\n//! through — no torn writes.\n\nuse std::io::{Read, Write};\nuse std::sync::{Arc, Mutex};\n\nuse portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};\n\nuse crate::surface::{SessionSurface, SurfaceError, SurfaceSize};\n\n/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain\n/// thread so the DSR auto-answer and caller input never interleave mid-write.\npub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;\n\n/// A child process running under a native PTY, exposed as a [`SessionSurface`].\n///\n/// Build one with [`PtySession::spawn`]; drive input via [`SessionSurface::write_input`]\n/// (or the A3 `send_keys`/`send_line` helpers), drain output via\n/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /\n/// [`PtySession::wait`] / [`PtySession::kill`].\npub struct PtySession {\n    /// The master end, behind a lock so `resize` never races the reader (Spike #5).\n    master: Mutex<Box<dyn MasterPty + Send>>,\n    /// The single PTY writer, shared so the A2 drain thread can answer DSR.\n    writer: SharedWriter,\n    /// The spawned child; behind a lock for `wait`/`try_wait`.\n    child: Mutex<Box<dyn Child + Send + Sync>>,\n    /// A kill handle split from the child so we can terminate from another\n    /// thread even while `wait` blocks (`portable-pty` `ChildKiller`).\n    killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,\n    /// The master reader, taken at most once (`take_reader`).\n    reader: Mutex<Option<Box<dyn Read + Send>>>,\n    /// The child OS pid, captured at spawn.\n    pid: Option<u32>,\n}\n\nimpl PtySession {\n    /// Spawn `cmd` under a fresh native PTY sized to `size`.\n    ///\n    /// The slave end is dropped immediately after the child is spawned: the\n    /// child holds the only remaining slave handle, which lets EOF propagate on\n    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF\n    /// behaviour is governed by the writer, handled by the A2 drain discipline.)\n    //\n    // [impl->REQ-TERM-1]\n    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {\n        let pty_system = native_pty_system();\n        let pair = pty_system\n            .openpty(size.into())\n            .map_err(|e| SurfaceError::OpenPty(e.to_string()))?;\n\n        let child = pair\n            .slave\n            .spawn_command(cmd)\n            .map_err(|e| SurfaceError::Spawn(e.to_string()))?;\n        // Drop the slave so the child owns the only slave-side handle.\n        drop(pair.slave);\n\n        let reader = pair\n            .master\n            .try_clone_reader()\n            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n        let writer = pair\n            .master\n            .take_writer()\n            .map_err(|e| SurfaceError::Io(io_other(e)))?;\n        let killer = child.clone_killer();\n        let pid = child.process_id();\n\n        Ok(Self {\n            master: Mutex::new(pair.master),\n            writer: Arc::new(Mutex::new(writer)),\n            child: Mutex::new(child),\n            killer: Mutex::new(killer),\n            reader: Mutex::new(Some(reader)),\n            pid,\n        })\n    }\n\n    /// Convenience: spawn `program` with `args` under a PTY sized to `size`,\n    /// inheriting the caller's working directory.\n    pub fn spawn_program<P, A, S>(\n        program: P,\n        args: A,\n        size: SurfaceSize,\n    ) -> Result<Self, SurfaceError>\n    where\n        P: AsRef<std::ffi::OsStr>,\n        A: IntoIterator<Item = S>,\n        S: AsRef<std::ffi::OsStr>,\n    {\n        Self::spawn_program_in(program, args, size, None::<&std::ffi::OsStr>)\n    }\n\n    /// Spawn `program` with `args` under a PTY sized to `size`, in `cwd` when\n    /// `Some` (REQ-HOST-RUN-2: a project-scoped working directory for the\n    /// spawned child) — `None` inherits the caller's cwd, identical to\n    /// [`Self::spawn_program`].\n    // [impl->REQ-HOST-RUN-2]\n    pub fn spawn_program_in<P, A, S, D>(\n        program: P,\n        args: A,\n        size: SurfaceSize,\n        cwd: Option<D>,\n    ) -> Result<Self, SurfaceError>\n    where\n        P: AsRef<std::ffi::OsStr>,\n        A: IntoIterator<Item = S>,\n        S: AsRef<std::ffi::OsStr>,\n        D: AsRef<std::ffi::OsStr>,\n    {\n        Self::spawn_program_in_env(program, args, size, cwd, std::iter::empty::<(&str, &str)>())\n    }\n\n    /// Like [`Self::spawn_program_in`] but also sets `env` vars on the child\n    /// (manifest `[env.<VAR>]` direction = inject — REQ-HAZARD-ENV-SUBST). Each\n    /// `(key, value)` is applied to the spawned process's environment ON TOP of\n    /// the inherited env, so an adapter's per-endpoint injects (e.g.\n    /// `SPT_ENDPOINT_ID = {id}`) actually reach the harness child.\n    // [impl->REQ-HAZARD-ENV-SUBST]\n    // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\n    pub fn spawn_program_in_env<P, A, S, D, E, K, V>(\n        program: P,\n        args: A,\n        size: SurfaceSize,\n        cwd: Option<D>,\n        env: E,\n    ) -> Result<Self, SurfaceError>\n    where\n        P: AsRef<std::ffi::OsStr>,\n        A: IntoIterator<Item = S>,\n        S: AsRef<std::ffi::OsStr>,\n        D: AsRef<std::ffi::OsStr>,\n        E: IntoIterator<Item = (K, V)>,\n        K: AsRef<std::ffi::OsStr>,\n        V: AsRef<std::ffi::OsStr>,\n    {\n        // Resolve the program for the OS spawn before portable-pty's own `which`\n        // can pick an extensionless shebang shim that CreateProcessW rejects\n        // (os error 193). On Windows this PATHEXT-resolves a bare name and wraps a\n        // non-PE target (.cmd/.bat → cmd.exe, .ps1 → powershell); off Windows it\n        // is a passthrough. The wrap prefix args precede the caller's args.\n        // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\n        let (program, prefix) = crate::winprog::resolve_for_pty(program.as_ref());\n        let mut cmd = CommandBuilder::new(&program);\n        for a in prefix {\n            cmd.arg(a);\n        }\n        for a in args {\n            cmd.arg(a);\n        }\n        if let Some(dir) = cwd {\n            cmd.cwd(dir);\n        }\n        for (k, v) in env {\n            cmd.env(k, v);\n        }\n        Self::spawn(cmd, size)\n    }\n\n    /// Take the master reader. Returns [`SurfaceError::ReaderTaken`] on the\n    /// second call — output is drained by exactly one consumer (the A2 drain\n    /// thread in the hosted path; a test consumer otherwise).\n    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {\n        self.reader\n            .lock()\n            .unwrap()\n            .take()\n            .ok_or(SurfaceError::ReaderTaken)\n    }\n\n    /// The shared writer handle — cloned to the A2 DSR drain thread so its\n    /// auto-answer goes through the same serialized writer the caller injects\n    /// input through (no torn writes).\n    pub(crate) fn shared_writer(&self) -> SharedWriter {\n        Arc::clone(&self.writer)\n    }\n\n    /// Poll for child exit without blocking.\n    pub fn try_wait(&self) -> Result<Option<ExitStatus>, SurfaceError> {\n        self.child\n            .lock()\n            .unwrap()\n            .try_wait()\n            .map_err(SurfaceError::Io)\n    }\n\n    /// Block until the child exits, yielding its status.\n    pub fn wait(&self) -> Result<ExitStatus, SurfaceError> {\n        self.child.lock().unwrap().wait().map_err(SurfaceError::Io)\n    }\n\n    /// Terminate the child. Safe to call from another thread while `wait`\n    /// blocks (uses the split `ChildKiller`). Confirm the reap via\n    /// [`PtySession::wait`] / [`PtySession::try_wait`].\n    pub fn kill(&self) -> Result<(), SurfaceError> {\n        let res = self.killer.lock().unwrap().kill();\n        #[cfg(windows)]\n        {\n            // portable-pty 0.8's Windows `ChildKiller` returns the spurious\n            // `last_os_error` even when `TerminateProcess` *succeeds* (the\n            // upstream success check is inverted; `WinChild::kill` swallows it,\n            // the cloned killer does not). Its Result is therefore untrustworthy\n            // — kill is best-effort; confirm death by reaping.\n            let _ = res;\n            Ok(())\n        }\n        #[cfg(unix)]\n        {\n            res.map_err(SurfaceError::Io)\n        }\n    }\n\n    /// The child's OS process id, if the backend exposes one.\n    pub fn process_id(&self) -> Option<u32> {\n        self.pid\n    }\n}\n\n// [impl->REQ-TERM-1]\nimpl SessionSurface for PtySession {\n    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\n        let mut w = self.writer.lock().unwrap();\n        w.write_all(bytes).map_err(SurfaceError::Io)?;\n        w.flush().map_err(SurfaceError::Io)\n    }\n\n    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError> {\n        // Hold the master lock so resize never races a concurrent reader/resize\n        // (Spike #5: resize races the reader on Windows).\n        self.master\n            .lock()\n            .unwrap()\n            .resize(size.into())\n            .map_err(|e| SurfaceError::Io(io_other(e)))\n    }\n}\n\n/// Fold a `portable-pty` error (a private `anyhow::Error` alias) into a\n/// `std::io::Error` so the crate's public surface never leaks `anyhow`. Generic\n/// over `Display` to avoid naming the un-exported error type.\nfn io_other<E: std::fmt::Display>(e: E) -> std::io::Error {\n    std::io::Error::other(e.to_string())\n}\n"}],"details":{"displayContent":{"text":"//! Native-PTY session backend — ConPTY on Windows 10+, `forkpty(3)` on Unix,\r\n//! both via `portable-pty` (the exact crate + handoff shape Spikes #1 / #4\r\n//! proved).\r\n//!\r\n//! [`PtySession`] spawns a child under a real PTY, owns the master reader and\r\n//! writer, and serves the [`SessionSurface`] contract. It is **single-surface\r\n//! per handle** — one child's PTY, not the multiplexed fleet (that supervisor is\r\n//! the M3b broker).\r\n//!\r\n//! ## Concurrency discipline (Spike #5, binding)\r\n//!\r\n//! `resize()` races the reader thread on Windows; the master is therefore held\r\n//! behind a `Mutex` and every `resize` takes that lock. The writer is a shared\r\n//! `Arc<Mutex<…>>` so the A2 DSR drain thread can answer ConPTY's startup\r\n//! cursor query through the same serialized writer the caller injects input\r\n//! through — no torn writes.\r\n\r\nuse std::io::{Read, Write};\r\nuse std::sync::{Arc, Mutex};\r\n\r\nuse portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};\r\n\r\nuse crate::surface::{SessionSurface, SurfaceError, SurfaceSize};\r\n\r\n/// Shared, serialized handle to the PTY master writer. Cloned to the A2 drain\r\n/// thread so the DSR auto-answer and caller input never interleave mid-write.\r\npub(crate) type SharedWriter = Arc<Mutex<Box<dyn Write + Send>>>;\r\n\r\n/// A child process running under a native PTY, exposed as a [`SessionSurface`].\r\n///\r\n/// Build one with [`PtySession::spawn`]; drive input via [`SessionSurface::write_input`]\r\n/// (or the A3 `send_keys`/`send_line` helpers), drain output via\r\n/// [`PtySession::take_reader`], and observe lifecycle via [`PtySession::try_wait`] /\r\n/// [`PtySession::wait`] / [`PtySession::kill`].\r\npub struct PtySession {\r\n    /// The master end, behind a lock so `resize` never races the reader (Spike #5).\r\n    master: Mutex<Box<dyn MasterPty + Send>>,\r\n    /// The single PTY writer, shared so the A2 drain thread can answer DSR.\r\n    writer: SharedWriter,\r\n    /// The spawned child; behind a lock for `wait`/`try_wait`.\r\n    child: Mutex<Box<dyn Child + Send + Sync>>,\r\n    /// A kill handle split from the child so we can terminate from another\r\n    /// thread even while `wait` blocks (`portable-pty` `ChildKiller`).\r\n    killer: Mutex<Box<dyn ChildKiller + Send + Sync>>,\r\n    /// The master reader, taken at most once (`take_reader`).\r\n    reader: Mutex<Option<Box<dyn Read + Send>>>,\r\n    /// The child OS pid, captured at spawn.\r\n    pid: Option<u32>,\r\n}\r\n\r\nimpl PtySession {\r\n    /// Spawn `cmd` under a fresh native PTY sized to `size`.\r\n    ///\r\n    /// The slave end is dropped immediately after the child is spawned: the\r\n    /// child holds the only remaining slave handle, which lets EOF propagate on\r\n    /// the Unix raw-pipe backend once the child exits. (On Windows the no-EOF\r\n    /// behaviour is governed by the writer, handled by the A2 drain discipline.)\r\n    //\r\n    // [impl->REQ-TERM-1]\r\n    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {\r\n        let pty_system = native_pty_system();\r\n        let pair = pty_system\r\n            .openpty(size.into())\r\n            .map_err(|e| SurfaceError::OpenPty(e.to_string()))?;\r\n\r\n        let child = pair\r\n            .slave\r\n            .spawn_command(cmd)\r\n            .map_err(|e| SurfaceError::Spawn(e.to_string()))?;\r\n        // Drop the slave so the child owns the only slave-side handle.\r\n        drop(pair.slave);\r\n\r\n        let reader = pair\r\n            .master\r\n            .try_clone_reader()\r\n            .map_err(|e| SurfaceError::Io(io_other(e)))?;\r\n        let writer = pair\r\n            .master\r\n            .take_writer()\r\n            .map_err(|e| SurfaceError::Io(io_other(e)))?;\r\n        let killer = child.clone_killer();\r\n        let pid = child.process_id();\r\n\r\n        Ok(Self {\r\n            master: Mutex::new(pair.master),\r\n            writer: Arc::new(Mutex::new(writer)),\r\n            child: Mutex::new(child),\r\n            killer: Mutex::new(killer),\r\n            reader: Mutex::new(Some(reader)),\r\n            pid,\r\n        })\r\n    }\r\n\r\n    /// Convenience: spawn `program` with `args` under a PTY sized to `size`,\r\n    /// inheriting the caller's working directory.\r\n    pub fn spawn_program<P, A, S>(\r\n        program: P,\r\n        args: A,\r\n        size: SurfaceSize,\r\n    ) -> Result<Self, SurfaceError>\r\n    where\r\n        P: AsRef<std::ffi::OsStr>,\r\n        A: IntoIterator<Item = S>,\r\n        S: AsRef<std::ffi::OsStr>,\r\n    {\r\n        Self::spawn_program_in(program, args, size, None::<&std::ffi::OsStr>)\r\n    }\r\n\r\n    /// Spawn `program` with `args` under a PTY sized to `size`, in `cwd` when\r\n    /// `Some` (REQ-HOST-RUN-2: a project-scoped working directory for the\r\n    /// spawned child) — `None` inherits the caller's cwd, identical to\r\n    /// [`Self::spawn_program`].\r\n    // [impl->REQ-HOST-RUN-2]\r\n    pub fn spawn_program_in<P, A, S, D>(\r\n        program: P,\r\n        args: A,\r\n        size: SurfaceSize,\r\n        cwd: Option<D>,\r\n    ) -> Result<Self, SurfaceError>\r\n    where\r\n        P: AsRef<std::ffi::OsStr>,\r\n        A: IntoIterator<Item = S>,\r\n        S: AsRef<std::ffi::OsStr>,\r\n        D: AsRef<std::ffi::OsStr>,\r\n    {\r\n        Self::spawn_program_in_env(program, args, size, cwd, std::iter::empty::<(&str, &str)>())\r\n    }\r\n\r\n    /// Like [`Self::spawn_program_in`] but also sets `env` vars on the child\r\n    /// (manifest `[env.<VAR>]` direction = inject — REQ-HAZARD-ENV-SUBST). Each\r\n    /// `(key, value)` is applied to the spawned process's environment ON TOP of\r\n    /// the inherited env, so an adapter's per-endpoint injects (e.g.\r\n    /// `SPT_ENDPOINT_ID = {id}`) actually reach the harness child.\r\n    // [impl->REQ-HAZARD-ENV-SUBST]\r\n    // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\r\n    pub fn spawn_program_in_env<P, A, S, D, E, K, V>(\r\n        program: P,\r\n        args: A,\r\n        size: SurfaceSize,\r\n        cwd: Option<D>,\r\n        env: E,\r\n    ) -> Result<Self, SurfaceError>\r\n    where\r\n        P: AsRef<std::ffi::OsStr>,\r\n        A: IntoIterator<Item = S>,\r\n        S: AsRef<std::ffi::OsStr>,\r\n        D: AsRef<std::ffi::OsStr>,\r\n        E: IntoIterator<Item = (K, V)>,\r\n        K: AsRef<std::ffi::OsStr>,\r\n        V: AsRef<std::ffi::OsStr>,\r\n    {\r\n        // Resolve the program for the OS spawn before portable-pty's own `which`\r\n        // can pick an extensionless shebang shim that CreateProcessW rejects\r\n        // (os error 193). On Windows this PATHEXT-resolves a bare name and wraps a\r\n        // non-PE target (.cmd/.bat → cmd.exe, .ps1 → powershell); off Windows it\r\n        // is a passthrough. The wrap prefix args precede the caller's args.\r\n        // [impl->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE]\r\n        let (program, prefix) = crate::winprog::resolve_for_pty(program.as_ref());\r\n        let mut cmd = CommandBuilder::new(&program);\r\n        for a in prefix {\r\n            cmd.arg(a);\r\n        }\r\n        for a in args {\r\n            cmd.arg(a);\r\n        }\r\n        if let Some(dir) = cwd {\r\n            cmd.cwd(dir);\r\n        }\r\n        for (k, v) in env {\r\n            cmd.env(k, v);\r\n        }\r\n        Self::spawn(cmd, size)\r\n    }\r\n\r\n    /// Take the master reader. Returns [`SurfaceError::ReaderTaken`] on the\r\n    /// second call — output is drained by exactly one consumer (the A2 drain\r\n    /// thread in the hosted path; a test consumer otherwise).\r\n    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {\r\n        self.reader\r\n            .lock()\r\n            .unwrap()\r\n            .take()\r\n            .ok_or(SurfaceError::ReaderTaken)\r\n    }\r\n\r\n    /// The shared writer handle — cloned to the A2 DSR drain thread so its\r\n    /// auto-answer goes through the same serialized writer the caller injects\r\n    /// input through (no torn writes).\r\n    pub(crate) fn shared_writer(&self) -> SharedWriter {\r\n        Arc::clone(&self.writer)\r\n    }\r\n\r\n    /// Poll for child exit without blocking.\r\n    pub fn try_wait(&self) -> Result<Option<ExitStatus>, SurfaceError> {\r\n        self.child\r\n            .lock()\r\n            .unwrap()\r\n            .try_wait()\r\n            .map_err(SurfaceError::Io)\r\n    }\r\n\r\n    /// Block until the child exits, yielding its status.\r\n    pub fn wait(&self) -> Result<ExitStatus, SurfaceError> {\r\n        self.child.lock().unwrap().wait().map_err(SurfaceError::Io)\r\n    }\r\n\r\n    /// Terminate the child. Safe to call from another thread while `wait`\r\n    /// blocks (uses the split `ChildKiller`). Confirm the reap via\r\n    /// [`PtySession::wait`] / [`PtySession::try_wait`].\r\n    pub fn kill(&self) -> Result<(), SurfaceError> {\r\n        let res = self.killer.lock().unwrap().kill();\r\n        #[cfg(windows)]\r\n        {\r\n            // portable-pty 0.8's Windows `ChildKiller` returns the spurious\r\n            // `last_os_error` even when `TerminateProcess` *succeeds* (the\r\n            // upstream success check is inverted; `WinChild::kill` swallows it,\r\n            // the cloned killer does not). Its Result is therefore untrustworthy\r\n            // — kill is best-effort; confirm death by reaping.\r\n            let _ = res;\r\n            Ok(())\r\n        }\r\n        #[cfg(unix)]\r\n        {\r\n            res.map_err(SurfaceError::Io)\r\n        }\r\n    }\r\n\r\n    /// The child's OS process id, if the backend exposes one.\r\n    pub fn process_id(&self) -> Option<u32> {\r\n        self.pid\r\n    }\r\n}\r\n\r\n// [impl->REQ-TERM-1]\r\nimpl SessionSurface for PtySession {\r\n    fn write_input(&self, bytes: &[u8]) -> Result<(), SurfaceError> {\r\n        let mut w = self.writer.lock().unwrap();\r\n        w.write_all(bytes).map_err(SurfaceError::Io)?;\r\n        w.flush().map_err(SurfaceError::Io)\r\n    }\r\n\r\n    fn resize(&self, size: SurfaceSize) -> Result<(), SurfaceError> {\r\n        // Hold the master lock so resize never races a concurrent reader/resize\r\n        // (Spike #5: resize races the reader on Windows).\r\n        self.master\r\n            .lock()\r\n            .unwrap()\r\n            .resize(size.into())\r\n            .map_err(|e| SurfaceError::Io(io_other(e)))\r\n    }\r\n}\r\n\r\n/// Fold a `portable-pty` error (a private `anyhow::Error` alias) into a\r\n/// `std::io::Error` so the crate's public surface never leaks `anyhow`. Generic\r\n/// over `Display` to avoid naming the un-exported error type.\r\nfn io_other<E: std::fmt::Display>(e: E) -> std::io::Error {\r\n    std::io::Error::other(e.to_string())\r\n}\r\n","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\pty.rs"}}},"isError":false,"timestamp":1784365520695}}
{"type":"message","id":"edf59cd9","parentId":"81b1ada3","timestamp":"2026-07-18T09:05:20.702Z","message":{"role":"toolResult","toolCallId":"call_GtkRDPpCpWeMGpXiHjTMayZn|fc_0ce5441d29b9f05d016a5b41d06010819bb27a59ac4cebe57a","toolName":"read","content":[{"type":"text","text":"//! Session-digest **log projection** — the live activity buffer built from an\n//! endpoint's normalized session logs, never from PTY bytes (ADR-0008 amendment,\n//! 2026-06-12; REQ-TERM-4 log-source re-foundation).\n//!\n//! ## Why logs, not the byte stream\n//!\n//! A TUI's PTY stream is a *rendering protocol*, not a content log: scroll/resize\n//! trigger full repaints that re-enter the stream (duplicate content) and drown\n//! the semantic turn boundaries in repaint traffic. The split (CONTEXT.md\n//! \"session digest\"): **render surfaces read the PTY** (the raw byte ring,\n//! unchanged); **content surfaces read the logs.** The digest is a rolling\n//! tail-projection of the **same normalized history the echo-commune consumes**\n//! (history subsystem Path A locate+normalize / Path B native store) — one\n//! pipeline, two consumers.\n//!\n//! ## The published digest-record contract (one schema, no mapping layer)\n//!\n//! The history subsystem stays **opaque** — it carries each record's `raw` line\n//! verbatim, adapter-defined, and never parses it (echo pipes those lines raw to\n//! an adapter summarizer). This projection does NOT make history non-opaque:\n//! [`DigestRecord`] is a **spt-core-published contract** — a small, fixed overlay\n//! on the *content* of `raw` that a projectable adapter targets (or pushes\n//! pre-formed via `api digest-entry`). It is the digest's own surface, not the\n//! history subsystem's, and it is the **only** digest record schema (no\n//! second-schema mapping). A normalized line is a JSON object:\n//!\n//! ```json\n//! {\"role\": \"input\", \"text\": \"fix the bug\", \"ts\": \"2026-06-13T21:00:00Z\"}\n//! {\"role\": \"agent\", \"text\": \"on it\"}\n//! {\"role\": \"tool\",  \"tool\": {\"name\": \"Write\", \"arg\": \"src/a.rs\"}}\n//! ```\n//!\n//! - `role` is the closed set `input` | `agent` | `tool`.\n//! - `text` carries the input / agent span (omitted/empty for `tool`).\n//! - `tool` is `{name, arg}`, present iff `role == \"tool\"`.\n//! - `ts` is an optional RFC3339-UTC ordering key (ADR-0019); the two-origin\n//!   merge interleaves extracted activity with spt's injected context by it.\n//!   Absent on older records — they keep stream order.\n//!\n//! **Forward-compat:** unknown object fields are ignored (a future contract field\n//! does not break an older reader); a line that is not a valid record — malformed\n//! JSON, missing/unknown `role`, or a `tool` record without its `tool` object —\n//! is **skipped**, never a panic and never an aborted projection.\n//!\n//! ## Presentation (unchanged from the original conception)\n//!\n//! The formula is spt-core-owned: the last [`DigestConfig::window_turns`] user\n//! turns (~3), consecutive tool records collapsed into one `ToolSprint`, and\n//! tool args truncated to [`DigestConfig::arg_truncation`] (~25) chars. Only the\n//! *source* changed (records, not regex-over-bytes); the projected [`Digest`]\n//! shape and its rendering are identical to the superseded parser's output.\n// [impl->REQ-TERM-4]\n\nuse serde::Deserialize;\n\nuse crate::digest::{truncate_arg, Digest, DigestConfig, DigestEntry, ToolUse, Turn};\n\n/// The role of a [`DigestRecord`] — the closed source-tag set. Deserialized from\n/// the lowercase wire token; an unrecognized token fails the record (→ skipped),\n/// which is exactly the forward-compat \"unknown role → skip\" behavior.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum DigestRole {\n    /// A user-input span opens a new turn.\n    Input,\n    /// Agent output within the current turn.\n    Agent,\n    /// One tool invocation (collapsed into the trailing sprint).\n    Tool,\n}\n\n/// The `{name, arg}` of a `role: \"tool\"` record. `arg` is the *untruncated*\n/// fragment as the adapter emitted it; presentation truncation is applied during\n/// projection (extraction is the adapter's, presentation is spt-core's).\n#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]\npub struct ToolRef {\n    /// The tool name (e.g. `Write`).\n    pub name: String,\n    /// The argument fragment (truncated at projection time).\n    #[serde(default)]\n    pub arg: String,\n}\n\n/// One record of the **published digest-record contract** — the spt-core-owned\n/// overlay a projectable adapter targets in its normalized history line. Unknown\n/// fields are ignored (forward-compat); the history subsystem still carries the\n/// underlying line opaquely.\n#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]\npub struct DigestRecord {\n    /// The source tag (input / agent / tool).\n    pub role: DigestRole,\n    /// The span text (input or agent). Empty/absent for a tool record.\n    #[serde(default)]\n    pub text: String,\n    /// The tool reference — present iff `role == Tool`.\n    #[serde(default)]\n    pub tool: Option<ToolRef>,\n    /// Optional RFC3339-UTC ordering key (the `ts` contract field, ADR-0019).\n    /// The two-origin merge interleaves extracted activity records with spt's\n    /// own context-injection entries by this key (lexical order on the\n    /// normalized string). Absent on older records (forward/backward compat) —\n    /// such records keep their stream order.\n    // [impl->REQ-TERM-5]\n    #[serde(default)]\n    pub ts: Option<String>,\n}\n\n/// Why a normalized line was dropped from the projection — the skip-diagnostics\n/// (ADR-0019). The silent `from_str().ok()` is gone: every drop now carries a\n/// reason a `digest-proof` run (or the runtime warn) can surface.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum DropReason {\n    /// Not valid contract JSON: malformed, a non-object, or an unknown/missing\n    /// `role` (serde's message — e.g. \"unknown variant `whisper`\" — is carried).\n    Malformed(String),\n    /// A `role: \"tool\"` record without its required `tool` object.\n    ToolMissingObject,\n}\n\nimpl std::fmt::Display for DropReason {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            DropReason::Malformed(msg) => write!(f, \"not a valid contract record: {msg}\"),\n            DropReason::ToolMissingObject => {\n                write!(f, \"role \\\"tool\\\" without a `tool` object\")\n            }\n        }\n    }\n}\n\n/// One dropped line, with its 1-based position, raw text, and reason — the unit\n/// of the skip-diagnostics surfaced by `spt adapter digest-proof`.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct DroppedLine {\n    pub line_no: usize,\n    pub raw: String,\n    pub reason: DropReason,\n}\n\n/// Skip-diagnostics for one projection: how many records parsed and every line\n/// that was dropped (with why). The author-time + runtime answer to \"the digest\n/// returns nothing\" (ADR-0019 §2).\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct DigestDiagnostics {\n    /// Count of lines that parsed into a valid contract record.\n    pub parsed: usize,\n    /// Every dropped line, in source order.\n    pub dropped: Vec<DroppedLine>,\n}\n\nimpl DigestDiagnostics {\n    /// Number of dropped lines.\n    pub fn drop_count(&self) -> usize {\n        self.dropped.len()\n    }\n    /// The first dropped line (the \"first mismatch\" an author chases).\n    pub fn first_drop(&self) -> Option<&DroppedLine> {\n        self.dropped.first()\n    }\n}\n\n/// Parse one normalized line into a [`DigestRecord`] per the published contract,\n/// returning the [`DropReason`] when it is not a valid record (malformed JSON, an\n/// unknown/missing `role`, or a `tool` record missing its `tool` object). Unknown\n/// *fields* on an otherwise-valid record are tolerated (ignored). This is the\n/// diagnostic core; [`record_to_tagged`] is the lossy `Option` wrapper.\n// [impl->REQ-TERM-5]\npub fn record_to_tagged_result(raw: &str) -> Result<DigestRecord, DropReason> {\n    let rec: DigestRecord =\n        serde_json::from_str(raw).map_err(|e| DropReason::Malformed(e.to_string()))?;\n    if rec.role == DigestRole::Tool && rec.tool.is_none() {\n        return Err(DropReason::ToolMissingObject);\n    }\n    Ok(rec)\n}\n\n/// Parse one normalized line into a [`DigestRecord`] per the published contract.\n/// Returns `None` for a line that is not a valid record (forward-compat skip);\n/// use [`record_to_tagged_result`] when the drop *reason* matters.\n// [impl->REQ-TERM-4]\npub fn record_to_tagged(raw: &str) -> Option<DigestRecord> {\n    record_to_tagged_result(raw).ok()\n}\n\n/// Project a sequence of contract records into the rolling [`Digest`]: fold them\n/// into turns (an `input` opens a turn; `agent` appends text; consecutive `tool`\n/// records collapse into one `ToolSprint`), then keep the last\n/// `config.window_turns`. A `tool` record missing its `tool` object is skipped\n/// (defensive — the contract requires it present for the tool role).\n// [impl->REQ-TERM-4]\npub fn project(records: &[DigestRecord], config: &DigestConfig) -> Digest {\n    let mut turns: Vec<Turn> = Vec::new();\n    let mut current = Turn::default();\n    let mut started = false;\n\n    for r in records {\n        match r.role {\n            DigestRole::Input => {\n                if started {\n                    turns.push(std::mem::take(&mut current));\n                }\n                current = Turn {\n                    input: Some(r.text.clone()),\n                    entries: Vec::new(),\n                    // The contract fold has no source ordinal — seq stays None.\n                    ..Default::default()\n                };\n                started = true;\n            }\n            DigestRole::Agent => {\n                if !r.text.is_empty() {\n                    // The contract fold has no source ordinal → seq stays None (the\n                    // daemon timeline is the seq source). [impl->REQ-DIGEST-CURSOR]\n                    current.entries.push(DigestEntry::agent(r.text.clone()));\n                    started = true;\n                }\n            }\n            DigestRole::Tool => {\n                let Some(t) = &r.tool else {\n                    continue; // tool role without a tool object → malformed, skip\n                };\n                let tu = ToolUse {\n                    name: t.name.clone(),\n                    arg: truncate_arg(t.arg.trim(), config.arg_truncation),\n                };\n                // Collapse into the trailing sprint when the last entry is one\n                // and collapsing is enabled; otherwise each tool is its own sprint.\n                match current.entries.last_mut() {\n                    Some(DigestEntry::ToolSprint { tools, .. }) if config.sprint_collapse => {\n                        tools.push(tu);\n                    }\n                    _ => current.entries.push(DigestEntry::tool_sprint(vec![tu])),\n                }\n                started = true;\n            }\n        }\n    }\n    if started {\n        turns.push(current);\n    }\n\n    // Keep only the last N turns (the rolling window).\n    let n = config.window_turns;\n    if turns.len() > n {\n        turns.drain(..turns.len() - n);\n    }\n    Digest { turns }\n}\n\n/// Tail-projection convenience: parse a sequence of normalized lines (skipping\n/// any that are not valid contract records) and project them. The single entry a\n/// log-sourced digest engine calls with the endpoint's current normalized\n/// history records.\n// [impl->REQ-TERM-4]\npub fn project_lines<'a, I>(lines: I, config: &DigestConfig) -> Digest\nwhere\n    I: IntoIterator<Item = &'a str>,\n{\n    project_lines_diagnosed(lines, config).0\n}\n\n// ───────────────────────── two-origin timeline (ADR-0019 / REQ-TERM-7) ──────\n//\n// The digest interleaves two origins by `ts`: the adapter's extracted **activity**\n// records and spt's own **context-injection** entries (psyche download, echo\n// mirror, owl message). A [`ContextRecord`] is spt-owned — adapters never emit\n// one; spt appends them to the endpoint's `digest.log`. The discriminator from an\n// activity record is the top-level `context_kind` field (activity records key on\n// `role`), so the two never alias and an unknown line still drops with a reason.\n\n/// One spt-injected context-injection record (REQ-TERM-7) — the `digest.log`\n/// shape spt writes (never an adapter). Discriminated from an activity record by\n/// its top-level `context_kind`.\n#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]\npub struct ContextRecord {\n    /// The injection subtype (`psyche_download` | `echo_mirror` | `owl_message`).\n    pub context_kind: String,\n    /// The injected context summary line.\n    #[serde(default)]\n    pub body: String,\n    /// The injection's RFC3339-UTC instant (the merge ordering key).\n    #[serde(default)]\n    pub ts: Option<String>,\n}\n\n/// Parse a `digest.log` line as an spt context-injection record — `Some` **iff**\n/// it carries the `context_kind` discriminator. A line without it is not a context\n/// record (it is an activity record, or garbage); the caller falls through to the\n/// activity parse.\n// [impl->REQ-TERM-7]\npub fn parse_context_record(raw: &str) -> Option<ContextRecord> {\n    let v: serde_json::Value = serde_json::from_str(raw).ok()?;\n    v.get(\"context_kind\")?;\n    serde_json::from_str(raw).ok()\n}\n\n/// One item on the merged digest timeline: an adapter **activity** record, an\n/// spt-injected **context** entry, or a session **boundary** divider. The daemon\n/// builds the time-ordered sequence (the two-origin merge + session splicing) and\n/// [`project_timeline`] folds it into the [`Digest`].\n#[derive(Debug, Clone, PartialEq)]\npub enum TimelineItem {\n    /// An adapter-extracted activity record plus its **digest-cursor seq**\n    /// (REQ-DIGEST-CURSOR) — `(ledger_ordinal << 32) | per_session_line_idx`,\n    /// computed by the daemon when it builds the timeline. The seq is the stable\n    /// cursor key threaded onto the folded entry/turn.\n    Activity {\n        record: DigestRecord,\n        seq: u64,\n    },\n    /// An spt-injected context entry (`kind`, `body`, `ts`).\n    Context {\n        kind: String,\n        body: String,\n        ts: Option<String>,\n    },\n    /// A session boundary (`/clear` | `/compact` | `boot`) divider.\n    Boundary { kind: String, ts: Option<String> },\n}\n\n/// Fold a pre-ordered timeline (two-origin-merged + boundary-spliced by the\n/// daemon) into the rolling [`Digest`], then window. An `Activity` input opens a\n/// turn (agent/tool append/collapse as in [`project`]); a `Context` entry appends\n\n[Showing lines 1-323 of 1025. Use :324 to continue]"}],"details":{"truncation":{"content":"//! Session-digest **log projection** — the live activity buffer built from an\n//! endpoint's normalized session logs, never from PTY bytes (ADR-0008 amendment,\n//! 2026-06-12; REQ-TERM-4 log-source re-foundation).\n//!\n//! ## Why logs, not the byte stream\n//!\n//! A TUI's PTY stream is a *rendering protocol*, not a content log: scroll/resize\n//! trigger full repaints that re-enter the stream (duplicate content) and drown\n//! the semantic turn boundaries in repaint traffic. The split (CONTEXT.md\n//! \"session digest\"): **render surfaces read the PTY** (the raw byte ring,\n//! unchanged); **content surfaces read the logs.** The digest is a rolling\n//! tail-projection of the **same normalized history the echo-commune consumes**\n//! (history subsystem Path A locate+normalize / Path B native store) — one\n//! pipeline, two consumers.\n//!\n//! ## The published digest-record contract (one schema, no mapping layer)\n//!\n//! The history subsystem stays **opaque** — it carries each record's `raw` line\n//! verbatim, adapter-defined, and never parses it (echo pipes those lines raw to\n//! an adapter summarizer). This projection does NOT make history non-opaque:\n//! [`DigestRecord`] is a **spt-core-published contract** — a small, fixed overlay\n//! on the *content* of `raw` that a projectable adapter targets (or pushes\n//! pre-formed via `api digest-entry`). It is the digest's own surface, not the\n//! history subsystem's, and it is the **only** digest record schema (no\n//! second-schema mapping). A normalized line is a JSON object:\n//!\n//! ```json\n//! {\"role\": \"input\", \"text\": \"fix the bug\", \"ts\": \"2026-06-13T21:00:00Z\"}\n//! {\"role\": \"agent\", \"text\": \"on it\"}\n//! {\"role\": \"tool\",  \"tool\": {\"name\": \"Write\", \"arg\": \"src/a.rs\"}}\n//! ```\n//!\n//! - `role` is the closed set `input` | `agent` | `tool`.\n//! - `text` carries the input / agent span (omitted/empty for `tool`).\n//! - `tool` is `{name, arg}`, present iff `role == \"tool\"`.\n//! - `ts` is an optional RFC3339-UTC ordering key (ADR-0019); the two-origin\n//!   merge interleaves extracted activity with spt's injected context by it.\n//!   Absent on older records — they keep stream order.\n//!\n//! **Forward-compat:** unknown object fields are ignored (a future contract field\n//! does not break an older reader); a line that is not a valid record — malformed\n//! JSON, missing/unknown `role`, or a `tool` record without its `tool` object —\n//! is **skipped**, never a panic and never an aborted projection.\n//!\n//! ## Presentation (unchanged from the original conception)\n//!\n//! The formula is spt-core-owned: the last [`DigestConfig::window_turns`] user\n//! turns (~3), consecutive tool records collapsed into one `ToolSprint`, and\n//! tool args truncated to [`DigestConfig::arg_truncation`] (~25) chars. Only the\n//! *source* changed (records, not regex-over-bytes); the projected [`Digest`]\n//! shape and its rendering are identical to the superseded parser's output.\n// [impl->REQ-TERM-4]\n\nuse serde::Deserialize;\n\nuse crate::digest::{truncate_arg, Digest, DigestConfig, DigestEntry, ToolUse, Turn};\n\n/// The role of a [`DigestRecord`] — the closed source-tag set. Deserialized from\n/// the lowercase wire token; an unrecognized token fails the record (→ skipped),\n/// which is exactly the forward-compat \"unknown role → skip\" behavior.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum DigestRole {\n    /// A user-input span opens a new turn.\n    Input,\n    /// Agent output within the current turn.\n    Agent,\n    /// One tool invocation (collapsed into the trailing sprint).\n    Tool,\n}\n\n/// The `{name, arg}` of a `role: \"tool\"` record. `arg` is the *untruncated*\n/// fragment as the adapter emitted it; presentation truncation is applied during\n/// projection (extraction is the adapter's, presentation is spt-core's).\n#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]\npub struct ToolRef {\n    /// The tool name (e.g. `Write`).\n    pub name: String,\n    /// The argument fragment (truncated at projection time).\n    #[serde(default)]\n    pub arg: String,\n}\n\n/// One record of the **published digest-record contract** — the spt-core-owned\n/// overlay a projectable adapter targets in its normalized history line. Unknown\n/// fields are ignored (forward-compat); the history subsystem still carries the\n/// underlying line opaquely.\n#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]\npub struct DigestRecord {\n    /// The source tag (input / agent / tool).\n    pub role: DigestRole,\n    /// The span text (input or agent). Empty/absent for a tool record.\n    #[serde(default)]\n    pub text: String,\n    /// The tool reference — present iff `role == Tool`.\n    #[serde(default)]\n    pub tool: Option<ToolRef>,\n    /// Optional RFC3339-UTC ordering key (the `ts` contract field, ADR-0019).\n    /// The two-origin merge interleaves extracted activity records with spt's\n    /// own context-injection entries by this key (lexical order on the\n    /// normalized string). Absent on older records (forward/backward compat) —\n    /// such records keep their stream order.\n    // [impl->REQ-TERM-5]\n    #[serde(default)]\n    pub ts: Option<String>,\n}\n\n/// Why a normalized line was dropped from the projection — the skip-diagnostics\n/// (ADR-0019). The silent `from_str().ok()` is gone: every drop now carries a\n/// reason a `digest-proof` run (or the runtime warn) can surface.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum DropReason {\n    /// Not valid contract JSON: malformed, a non-object, or an unknown/missing\n    /// `role` (serde's message — e.g. \"unknown variant `whisper`\" — is carried).\n    Malformed(String),\n    /// A `role: \"tool\"` record without its required `tool` object.\n    ToolMissingObject,\n}\n\nimpl std::fmt::Display for DropReason {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            DropReason::Malformed(msg) => write!(f, \"not a valid contract record: {msg}\"),\n            DropReason::ToolMissingObject => {\n                write!(f, \"role \\\"tool\\\" without a `tool` object\")\n            }\n        }\n    }\n}\n\n/// One dropped line, with its 1-based position, raw text, and reason — the unit\n/// of the skip-diagnostics surfaced by `spt adapter digest-proof`.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct DroppedLine {\n    pub line_no: usize,\n    pub raw: String,\n    pub reason: DropReason,\n}\n\n/// Skip-diagnostics for one projection: how many records parsed and every line\n/// that was dropped (with why). The author-time + runtime answer to \"the digest\n/// returns nothing\" (ADR-0019 §2).\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct DigestDiagnostics {\n    /// Count of lines that parsed into a valid contract record.\n    pub parsed: usize,\n    /// Every dropped line, in source order.\n    pub dropped: Vec<DroppedLine>,\n}\n\nimpl DigestDiagnostics {\n    /// Number of dropped lines.\n    pub fn drop_count(&self) -> usize {\n        self.dropped.len()\n    }\n    /// The first dropped line (the \"first mismatch\" an author chases).\n    pub fn first_drop(&self) -> Option<&DroppedLine> {\n        self.dropped.first()\n    }\n}\n\n/// Parse one normalized line into a [`DigestRecord`] per the published contract,\n/// returning the [`DropReason`] when it is not a valid record (malformed JSON, an\n/// unknown/missing `role`, or a `tool` record missing its `tool` object). Unknown\n/// *fields* on an otherwise-valid record are tolerated (ignored). This is the\n/// diagnostic core; [`record_to_tagged`] is the lossy `Option` wrapper.\n// [impl->REQ-TERM-5]\npub fn record_to_tagged_result(raw: &str) -> Result<DigestRecord, DropReason> {\n    let rec: DigestRecord =\n        serde_json::from_str(raw).map_err(|e| DropReason::Malformed(e.to_string()))?;\n    if rec.role == DigestRole::Tool && rec.tool.is_none() {\n        return Err(DropReason::ToolMissingObject);\n    }\n    Ok(rec)\n}\n\n/// Parse one normalized line into a [`DigestRecord`] per the published contract.\n/// Returns `None` for a line that is not a valid record (forward-compat skip);\n/// use [`record_to_tagged_result`] when the drop *reason* matters.\n// [impl->REQ-TERM-4]\npub fn record_to_tagged(raw: &str) -> Option<DigestRecord> {\n    record_to_tagged_result(raw).ok()\n}\n\n/// Project a sequence of contract records into the rolling [`Digest`]: fold them\n/// into turns (an `input` opens a turn; `agent` appends text; consecutive `tool`\n/// records collapse into one `ToolSprint`), then keep the last\n/// `config.window_turns`. A `tool` record missing its `tool` object is skipped\n/// (defensive — the contract requires it present for the tool role).\n// [impl->REQ-TERM-4]\npub fn project(records: &[DigestRecord], config: &DigestConfig) -> Digest {\n    let mut turns: Vec<Turn> = Vec::new();\n    let mut current = Turn::default();\n    let mut started = false;\n\n    for r in records {\n        match r.role {\n            DigestRole::Input => {\n                if started {\n                    turns.push(std::mem::take(&mut current));\n                }\n                current = Turn {\n                    input: Some(r.text.clone()),\n                    entries: Vec::new(),\n                    // The contract fold has no source ordinal — seq stays None.\n                    ..Default::default()\n                };\n                started = true;\n            }\n            DigestRole::Agent => {\n                if !r.text.is_empty() {\n                    // The contract fold has no source ordinal → seq stays None (the\n                    // daemon timeline is the seq source). [impl->REQ-DIGEST-CURSOR]\n                    current.entries.push(DigestEntry::agent(r.text.clone()));\n                    started = true;\n                }\n            }\n            DigestRole::Tool => {\n                let Some(t) = &r.tool else {\n                    continue; // tool role without a tool object → malformed, skip\n                };\n                let tu = ToolUse {\n                    name: t.name.clone(),\n                    arg: truncate_arg(t.arg.trim(), config.arg_truncation),\n                };\n                // Collapse into the trailing sprint when the last entry is one\n                // and collapsing is enabled; otherwise each tool is its own sprint.\n                match current.entries.last_mut() {\n                    Some(DigestEntry::ToolSprint { tools, .. }) if config.sprint_collapse => {\n                        tools.push(tu);\n                    }\n                    _ => current.entries.push(DigestEntry::tool_sprint(vec![tu])),\n                }\n                started = true;\n            }\n        }\n    }\n    if started {\n        turns.push(current);\n    }\n\n    // Keep only the last N turns (the rolling window).\n    let n = config.window_turns;\n    if turns.len() > n {\n        turns.drain(..turns.len() - n);\n    }\n    Digest { turns }\n}\n\n/// Tail-projection convenience: parse a sequence of normalized lines (skipping\n/// any that are not valid contract records) and project them. The single entry a\n/// log-sourced digest engine calls with the endpoint's current normalized\n/// history records.\n// [impl->REQ-TERM-4]\npub fn project_lines<'a, I>(lines: I, config: &DigestConfig) -> Digest\nwhere\n    I: IntoIterator<Item = &'a str>,\n{\n    project_lines_diagnosed(lines, config).0\n}\n\n// ───────────────────────── two-origin timeline (ADR-0019 / REQ-TERM-7) ──────\n//\n// The digest interleaves two origins by `ts`: the adapter's extracted **activity**\n// records and spt's own **context-injection** entries (psyche download, echo\n// mirror, owl message). A [`ContextRecord`] is spt-owned — adapters never emit\n// one; spt appends them to the endpoint's `digest.log`. The discriminator from an\n// activity record is the top-level `context_kind` field (activity records key on\n// `role`), so the two never alias and an unknown line still drops with a reason.\n\n/// One spt-injected context-injection record (REQ-TERM-7) — the `digest.log`\n/// shape spt writes (never an adapter). Discriminated from an activity record by\n/// its top-level `context_kind`.\n#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]\npub struct ContextRecord {\n    /// The injection subtype (`psyche_download` | `echo_mirror` | `owl_message`).\n    pub context_kind: String,\n    /// The injected context summary line.\n    #[serde(default)]\n    pub body: String,\n    /// The injection's RFC3339-UTC instant (the merge ordering key).\n    #[serde(default)]\n    pub ts: Option<String>,\n}\n\n/// Parse a `digest.log` line as an spt context-injection record — `Some` **iff**\n/// it carries the `context_kind` discriminator. A line without it is not a context\n/// record (it is an activity record, or garbage); the caller falls through to the\n/// activity parse.\n// [impl->REQ-TERM-7]\npub fn parse_context_record(raw: &str) -> Option<ContextRecord> {\n    let v: serde_json::Value = serde_json::from_str(raw).ok()?;\n    v.get(\"context_kind\")?;\n    serde_json::from_str(raw).ok()\n}\n\n/// One item on the merged digest timeline: an adapter **activity** record, an\n/// spt-injected **context** entry, or a session **boundary** divider. The daemon\n/// builds the time-ordered sequence (the two-origin merge + session splicing) and\n/// [`project_timeline`] folds it into the [`Digest`].\n#[derive(Debug, Clone, PartialEq)]\npub enum TimelineItem {\n    /// An adapter-extracted activity record plus its **digest-cursor seq**\n    /// (REQ-DIGEST-CURSOR) — `(ledger_ordinal << 32) | per_session_line_idx`,\n    /// computed by the daemon when it builds the timeline. The seq is the stable\n    /// cursor key threaded onto the folded entry/turn.\n    Activity {\n        record: DigestRecord,\n        seq: u64,\n    },\n    /// An spt-injected context entry (`kind`, `body`, `ts`).\n    Context {\n        kind: String,\n        body: String,\n        ts: Option<String>,\n    },\n    /// A session boundary (`/clear` | `/compact` | `boot`) divider.\n    Boundary { kind: String, ts: Option<String> },\n}\n\n/// Fold a pre-ordered timeline (two-origin-merged + boundary-spliced by the\n/// daemon) into the rolling [`Digest`], then window. An `Activity` input opens a\n/// turn (agent/tool append/collapse as in [`project`]); a `Context` entry appends","truncated":true,"truncatedBy":"lines","totalLines":1025,"totalBytes":14300,"outputLines":323,"outputBytes":14300,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Session-digest **log projection** — the live activity buffer built from an\n//! endpoint's normalized session logs, never from PTY bytes (ADR-0008 amendment,\n//! 2026-06-12; REQ-TERM-4 log-source re-foundation).\n//!\n//! ## Why logs, not the byte stream\n//!\n//! A TUI's PTY stream is a *rendering protocol*, not a content log: scroll/resize\n//! trigger full repaints that re-enter the stream (duplicate content) and drown\n//! the semantic turn boundaries in repaint traffic. The split (CONTEXT.md\n//! \"session digest\"): **render surfaces read the PTY** (the raw byte ring,\n//! unchanged); **content surfaces read the logs.** The digest is a rolling\n//! tail-projection of the **same normalized history the echo-commune consumes**\n//! (history subsystem Path A locate+normalize / Path B native store) — one\n//! pipeline, two consumers.\n//!\n//! ## The published digest-record contract (one schema, no mapping layer)\n//!\n//! The history subsystem stays **opaque** — it carries each record's `raw` line\n//! verbatim, adapter-defined, and never parses it (echo pipes those lines raw to\n//! an adapter summarizer). This projection does NOT make history non-opaque:\n//! [`DigestRecord`] is a **spt-core-published contract** — a small, fixed overlay\n//! on the *content* of `raw` that a projectable adapter targets (or pushes\n//! pre-formed via `api digest-entry`). It is the digest's own surface, not the\n//! history subsystem's, and it is the **only** digest record schema (no\n//! second-schema mapping). A normalized line is a JSON object:\n//!\n//! ```json\n//! {\"role\": \"input\", \"text\": \"fix the bug\", \"ts\": \"2026-06-13T21:00:00Z\"}\n//! {\"role\": \"agent\", \"text\": \"on it\"}\n//! {\"role\": \"tool\",  \"tool\": {\"name\": \"Write\", \"arg\": \"src/a.rs\"}}\n//! ```\n//!\n//! - `role` is the closed set `input` | `agent` | `tool`.\n//! - `text` carries the input / agent span (omitted/empty for `tool`).\n//! - `tool` is `{name, arg}`, present iff `role == \"tool\"`.\n//! - `ts` is an optional RFC3339-UTC ordering key (ADR-0019); the two-origin\n//!   merge interleaves extracted activity with spt's injected context by it.\n//!   Absent on older records — they keep stream order.\n//!\n//! **Forward-compat:** unknown object fields are ignored (a future contract field\n//! does not break an older reader); a line that is not a valid record — malformed\n//! JSON, missing/unknown `role`, or a `tool` record without its `tool` object —\n//! is **skipped**, never a panic and never an aborted projection.\n//!\n//! ## Presentation (unchanged from the original conception)\n//!\n//! The formula is spt-core-owned: the last [`DigestConfig::window_turns`] user\n//! turns (~3), consecutive tool records collapsed into one `ToolSprint`, and\n//! tool args truncated to [`DigestConfig::arg_truncation`] (~25) chars. Only the\n//! *source* changed (records, not regex-over-bytes); the projected [`Digest`]\n//! shape and its rendering are identical to the superseded parser's output.\n// [impl->REQ-TERM-4]\n\nuse serde::Deserialize;\n\nuse crate::digest::{truncate_arg, Digest, DigestConfig, DigestEntry, ToolUse, Turn};\n\n/// The role of a [`DigestRecord`] — the closed source-tag set. Deserialized from\n/// the lowercase wire token; an unrecognized token fails the record (→ skipped),\n/// which is exactly the forward-compat \"unknown role → skip\" behavior.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]\n#[serde(rename_all = \"lowercase\")]\npub enum DigestRole {\n    /// A user-input span opens a new turn.\n    Input,\n    /// Agent output within the current turn.\n    Agent,\n    /// One tool invocation (collapsed into the trailing sprint).\n    Tool,\n}\n\n/// The `{name, arg}` of a `role: \"tool\"` record. `arg` is the *untruncated*\n/// fragment as the adapter emitted it; presentation truncation is applied during\n/// projection (extraction is the adapter's, presentation is spt-core's).\n#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]\npub struct ToolRef {\n    /// The tool name (e.g. `Write`).\n    pub name: String,\n    /// The argument fragment (truncated at projection time).\n    #[serde(default)]\n    pub arg: String,\n}\n\n/// One record of the **published digest-record contract** — the spt-core-owned\n/// overlay a projectable adapter targets in its normalized history line. Unknown\n/// fields are ignored (forward-compat); the history subsystem still carries the\n/// underlying line opaquely.\n#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]\npub struct DigestRecord {\n    /// The source tag (input / agent / tool).\n    pub role: DigestRole,\n    /// The span text (input or agent). Empty/absent for a tool record.\n    #[serde(default)]\n    pub text: String,\n    /// The tool reference — present iff `role == Tool`.\n    #[serde(default)]\n    pub tool: Option<ToolRef>,\n    /// Optional RFC3339-UTC ordering key (the `ts` contract field, ADR-0019).\n    /// The two-origin merge interleaves extracted activity records with spt's\n    /// own context-injection entries by this key (lexical order on the\n    /// normalized string). Absent on older records (forward/backward compat) —\n    /// such records keep their stream order.\n    // [impl->REQ-TERM-5]\n    #[serde(default)]\n    pub ts: Option<String>,\n}\n\n/// Why a normalized line was dropped from the projection — the skip-diagnostics\n/// (ADR-0019). The silent `from_str().ok()` is gone: every drop now carries a\n/// reason a `digest-proof` run (or the runtime warn) can surface.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum DropReason {\n    /// Not valid contract JSON: malformed, a non-object, or an unknown/missing\n    /// `role` (serde's message — e.g. \"unknown variant `whisper`\" — is carried).\n    Malformed(String),\n    /// A `role: \"tool\"` record without its required `tool` object.\n    ToolMissingObject,\n}\n\nimpl std::fmt::Display for DropReason {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            DropReason::Malformed(msg) => write!(f, \"not a valid contract record: {msg}\"),\n            DropReason::ToolMissingObject => {\n                write!(f, \"role \\\"tool\\\" without a `tool` object\")\n            }\n        }\n    }\n}\n\n/// One dropped line, with its 1-based position, raw text, and reason — the unit\n/// of the skip-diagnostics surfaced by `spt adapter digest-proof`.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct DroppedLine {\n    pub line_no: usize,\n    pub raw: String,\n    pub reason: DropReason,\n}\n\n/// Skip-diagnostics for one projection: how many records parsed and every line\n/// that was dropped (with why). The author-time + runtime answer to \"the digest\n/// returns nothing\" (ADR-0019 §2).\n#[derive(Debug, Clone, Default, PartialEq, Eq)]\npub struct DigestDiagnostics {\n    /// Count of lines that parsed into a valid contract record.\n    pub parsed: usize,\n    /// Every dropped line, in source order.\n    pub dropped: Vec<DroppedLine>,\n}\n\nimpl DigestDiagnostics {\n    /// Number of dropped lines.\n    pub fn drop_count(&self) -> usize {\n        self.dropped.len()\n    }\n    /// The first dropped line (the \"first mismatch\" an author chases).\n    pub fn first_drop(&self) -> Option<&DroppedLine> {\n        self.dropped.first()\n    }\n}\n\n/// Parse one normalized line into a [`DigestRecord`] per the published contract,\n/// returning the [`DropReason`] when it is not a valid record (malformed JSON, an\n/// unknown/missing `role`, or a `tool` record missing its `tool` object). Unknown\n/// *fields* on an otherwise-valid record are tolerated (ignored). This is the\n/// diagnostic core; [`record_to_tagged`] is the lossy `Option` wrapper.\n// [impl->REQ-TERM-5]\npub fn record_to_tagged_result(raw: &str) -> Result<DigestRecord, DropReason> {\n    let rec: DigestRecord =\n        serde_json::from_str(raw).map_err(|e| DropReason::Malformed(e.to_string()))?;\n    if rec.role == DigestRole::Tool && rec.tool.is_none() {\n        return Err(DropReason::ToolMissingObject);\n    }\n    Ok(rec)\n}\n\n/// Parse one normalized line into a [`DigestRecord`] per the published contract.\n/// Returns `None` for a line that is not a valid record (forward-compat skip);\n/// use [`record_to_tagged_result`] when the drop *reason* matters.\n// [impl->REQ-TERM-4]\npub fn record_to_tagged(raw: &str) -> Option<DigestRecord> {\n    record_to_tagged_result(raw).ok()\n}\n\n/// Project a sequence of contract records into the rolling [`Digest`]: fold them\n/// into turns (an `input` opens a turn; `agent` appends text; consecutive `tool`\n/// records collapse into one `ToolSprint`), then keep the last\n/// `config.window_turns`. A `tool` record missing its `tool` object is skipped\n/// (defensive — the contract requires it present for the tool role).\n// [impl->REQ-TERM-4]\npub fn project(records: &[DigestRecord], config: &DigestConfig) -> Digest {\n    let mut turns: Vec<Turn> = Vec::new();\n    let mut current = Turn::default();\n    let mut started = false;\n\n    for r in records {\n        match r.role {\n            DigestRole::Input => {\n                if started {\n                    turns.push(std::mem::take(&mut current));\n                }\n                current = Turn {\n                    input: Some(r.text.clone()),\n                    entries: Vec::new(),\n                    // The contract fold has no source ordinal — seq stays None.\n                    ..Default::default()\n                };\n                started = true;\n            }\n            DigestRole::Agent => {\n                if !r.text.is_empty() {\n                    // The contract fold has no source ordinal → seq stays None (the\n                    // daemon timeline is the seq source). [impl->REQ-DIGEST-CURSOR]\n                    current.entries.push(DigestEntry::agent(r.text.clone()));\n                    started = true;\n                }\n            }\n            DigestRole::Tool => {\n                let Some(t) = &r.tool else {\n                    continue; // tool role without a tool object → malformed, skip\n                };\n                let tu = ToolUse {\n                    name: t.name.clone(),\n                    arg: truncate_arg(t.arg.trim(), config.arg_truncation),\n                };\n                // Collapse into the trailing sprint when the last entry is one\n                // and collapsing is enabled; otherwise each tool is its own sprint.\n                match current.entries.last_mut() {\n                    Some(DigestEntry::ToolSprint { tools, .. }) if config.sprint_collapse => {\n                        tools.push(tu);\n                    }\n                    _ => current.entries.push(DigestEntry::tool_sprint(vec![tu])),\n                }\n                started = true;\n            }\n        }\n    }\n    if started {\n        turns.push(current);\n    }\n\n    // Keep only the last N turns (the rolling window).\n    let n = config.window_turns;\n    if turns.len() > n {\n        turns.drain(..turns.len() - n);\n    }\n    Digest { turns }\n}\n\n/// Tail-projection convenience: parse a sequence of normalized lines (skipping\n/// any that are not valid contract records) and project them. The single entry a\n/// log-sourced digest engine calls with the endpoint's current normalized\n/// history records.\n// [impl->REQ-TERM-4]\npub fn project_lines<'a, I>(lines: I, config: &DigestConfig) -> Digest\nwhere\n    I: IntoIterator<Item = &'a str>,\n{\n    project_lines_diagnosed(lines, config).0\n}\n\n// ───────────────────────── two-origin timeline (ADR-0019 / REQ-TERM-7) ──────\n//\n// The digest interleaves two origins by `ts`: the adapter's extracted **activity**\n// records and spt's own **context-injection** entries (psyche download, echo\n// mirror, owl message). A [`ContextRecord`] is spt-owned — adapters never emit\n// one; spt appends them to the endpoint's `digest.log`. The discriminator from an\n// activity record is the top-level `context_kind` field (activity records key on\n// `role`), so the two never alias and an unknown line still drops with a reason.\n\n/// One spt-injected context-injection record (REQ-TERM-7) — the `digest.log`\n/// shape spt writes (never an adapter). Discriminated from an activity record by\n/// its top-level `context_kind`.\n#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]\npub struct ContextRecord {\n    /// The injection subtype (`psyche_download` | `echo_mirror` | `owl_message`).\n    pub context_kind: String,\n    /// The injected context summary line.\n    #[serde(default)]\n    pub body: String,\n    /// The injection's RFC3339-UTC instant (the merge ordering key).\n    #[serde(default)]\n    pub ts: Option<String>,\n}\n\n/// Parse a `digest.log` line as an spt context-injection record — `Some` **iff**\n/// it carries the `context_kind` discriminator. A line without it is not a context\n/// record (it is an activity record, or garbage); the caller falls through to the\n/// activity parse.\n// [impl->REQ-TERM-7]\npub fn parse_context_record(raw: &str) -> Option<ContextRecord> {\n    let v: serde_json::Value = serde_json::from_str(raw).ok()?;\n    v.get(\"context_kind\")?;\n    serde_json::from_str(raw).ok()\n}\n\n/// One item on the merged digest timeline: an adapter **activity** record, an\n/// spt-injected **context** entry, or a session **boundary** divider. The daemon\n/// builds the time-ordered sequence (the two-origin merge + session splicing) and\n/// [`project_timeline`] folds it into the [`Digest`].\n#[derive(Debug, Clone, PartialEq)]\npub enum TimelineItem {\n    /// An adapter-extracted activity record plus its **digest-cursor seq**\n    /// (REQ-DIGEST-CURSOR) — `(ledger_ordinal << 32) | per_session_line_idx`,\n    /// computed by the daemon when it builds the timeline. The seq is the stable\n    /// cursor key threaded onto the folded entry/turn.\n    Activity {\n        record: DigestRecord,\n        seq: u64,\n    },\n    /// An spt-injected context entry (`kind`, `body`, `ts`).\n    Context {\n        kind: String,\n        body: String,\n        ts: Option<String>,\n    },\n    /// A session boundary (`/clear` | `/compact` | `boot`) divider.\n    Boundary { kind: String, ts: Option<String> },\n}\n\n/// Fold a pre-ordered timeline (two-origin-merged + boundary-spliced by the\n/// daemon) into the rolling [`Digest`], then window. An `Activity` input opens a\n/// turn (agent/tool append/collapse as in [`project`]); a `Context` entry appends","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\projection.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1025,"totalBytes":14300,"outputLines":323,"outputBytes":14300,"shownRange":{"start":1,"end":323},"nextOffset":324}}},"isError":false,"timestamp":1784365520702}}
{"type":"message","id":"2d8730f8","parentId":"edf59cd9","timestamp":"2026-07-18T09:05:30.469Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Evaluating DSR handling in drain**"},{"type":"toolCall","id":"call_KD52YdZEYAcDb8C0rRnrqXrC|fc_0ce5441d29b9f05d016a5b41da63e8819ba9e77c890443830f","name":"read","arguments":{"i":"Reading drain byte forwarding","path":"crates/spt-term/src/reader.rs","selector":"1-220"}},{"type":"toolCall","id":"call_qXD8UacIVTfW1p0kJQKxKydm|fc_0ce5441d29b9f05d016a5b41da63f4819b9f4abae6313eb6ae","name":"grep","arguments":{"i":"Finding spawn drain waiter","pattern":"dispatch_spawn|drain\\(|wait\\(|stamp_reaped|ExitEvent|exit_envelope|remove\\(&|request_stop|join","path":"crates/spt-daemon/src/broker.rs","selector":"3500-3920","case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_MRwo6mezkw7I3hy00XLhdqy6|fc_0ce5441d29b9f05d016a5b41da63fc819b9e0b89b9746c677b","name":"read","arguments":{"i":"Reading session spawn teardown","path":"crates/spt-daemon/src/broker.rs","selector":"3500-3910"}},{"type":"toolCall","id":"call_tTGaAiMvee9vVmRKGD1sVVh9|fc_0ce5441d29b9f05d016a5b41da6404819bb9e59004d7ecd95e","name":"read","arguments":{"i":"Reading screen mode tail","path":"crates/spt-term/src/screen.rs","selector":"720-870"}},{"type":"toolCall","id":"call_5gAp76dtR0vhv1trljJ4iKMv|fc_0ce5441d29b9f05d016a5b41da6408819bb88087d83c1b4f67","name":"grep","arguments":{"i":"Finding cleanup hazards","pattern":"CONTROL-STAMP-LIFETIME|RC-EOF|RC-RECONNECT|terminal.*restore|alternate screen|cursor visibility|raw mode|cleanup","path":"docs/KNOWN-HAZARDS.md; docs/adr; docs/design","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":19924,"output":319,"cacheRead":146944,"cacheWrite":0,"totalTokens":167187,"reasoningTokens":25,"cost":{"input":0.09962000000000001,"output":0.00957,"cacheRead":0.073472,"cacheWrite":0,"total":0.182662}},"stopReason":"toolUse","timestamp":1784365520749,"responseId":"resp_0ce5441d29b9f05d016a5b41d127ec819b8d09399eef6a8257","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b41d498f0819b8779f43531733dbd","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0HaIA5ibiAf1dSdJHEX1i9JcEZj7dCjVCw5-W9BvMuCpBO6zZSJuRyo_xuvc3o5LQtXECNe4UMdiOXh-egwn-uESI-_3sP9ctlpSzUWDF8iqh6bIOl4LVf5aP7-YbV7JjYPocnVPeMcKflUopnaip3H8BuQWW6jXK9A3JcDpuRILNvJmMAR6cD4wXbW62wKfAECguKVskjN9LiHPtOv8NXWDNGGRCbU7RgNYp9f5FkBMjG28orbYxW5dimIgcOs2PBLIDmd8eevK6cm9p07cjkFrb9YqQYbqml_VXZZHp3TxWhGYbWj8xXUsTwVasF4-cGSzW3mgYxI8g-EvtgVVUGm0VGA18FuCxGXyxcNfjk_Pb4zLe_qAS5t001D3KcPXG_pccVtyM40mre1ZEfEax4I6cCxd8HWHr7SWaOms76cJ7yIE-Tw6bdOk10-GOMl02kCRN-eSlwCBqPBp4mvGpQvYxNmIoNBGjNaBbLRr7tl6zV7-NuT8UnysMkK6GJSYw9nFCKuOOZ08nM067A5mAS5XFhyJIL_IHu70mCxfvI08-Mqa4SmQNdONXxIzGjWtSakBdLGCZeHauPrcMM2cKhuLBGNzuB-lA7qL5OfOZwKmSo9O1uMBMGFLhUCZf3MJPwP2Ctvc_mEcQX4TXrfi2Lqi0TrfUTUGGDPpfjG3RctOWg96EtI73qSfbGNUWs7TZ7yu3eqDNmtz0VkjZplDrTYnkKjMd4ceI1a_vZr2mB2tnIPUmi36qm4nFP5CPxBu9Zg08cCfFL7Uo8lvIZZr8yrJLWtyHnyD_i5VSINsVx80biqNZcUoMlJIrQblso0aj9tK3ZNTA1w--dS5lUdFASlP2uqF9r2I_xWNZ9qwHy8D883XiC10aYamFdfS5VfmtlC5p2pxsQOh1ZALRqtm_YEqHgQBJhzmtNC1RC1LeznmEdBl5xjnRnBrV__5clyzibvsWyLBq4fpYXZjciNMCmbxyDseACU1Cm86wfLTi-Idg5lUJUwAqf5tgyxUuCLRS1BqknxH5ra6GByF0jZcR_4QQ==","summary":[{"type":"summary_text","text":"**Evaluating DSR handling in drain**"}]},{"id":"fc_0ce5441d29b9f05d016a5b41da63e8819ba9e77c890443830f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading drain byte forwarding\",\"path\":\"crates/spt-term/src/reader.rs\",\"selector\":\"1-220\"}","call_id":"call_KD52YdZEYAcDb8C0rRnrqXrC","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41da63f4819b9f4abae6313eb6ae","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding spawn drain waiter\",\"pattern\":\"dispatch_spawn|drain\\\\(|wait\\\\(|stamp_reaped|ExitEvent|exit_envelope|remove\\\\(&|request_stop|join\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"3500-3920\",\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_qXD8UacIVTfW1p0kJQKxKydm","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41da63fc819b9e0b89b9746c677b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading session spawn teardown\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"3500-3910\"}","call_id":"call_MRwo6mezkw7I3hy00XLhdqy6","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41da6404819bb9e59004d7ecd95e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading screen mode tail\",\"path\":\"crates/spt-term/src/screen.rs\",\"selector\":\"720-870\"}","call_id":"call_tTGaAiMvee9vVmRKGD1sVVh9","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41da6408819bb88087d83c1b4f67","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding cleanup hazards\",\"pattern\":\"CONTROL-STAMP-LIFETIME|RC-EOF|RC-RECONNECT|terminal.*restore|alternate screen|cursor visibility|raw mode|cleanup\",\"path\":\"docs/KNOWN-HAZARDS.md; docs/adr; docs/design\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_5gAp76dtR0vhv1trljJ4iKMv","name":"grep"}]},"duration":9719.43900001049,"ttft":3844.5502000153065,"contextSnapshot":{"promptTokens":166868,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KD52YdZEYAcDb8C0rRnrqXrC|fc_0ce5441d29b9f05d016a5b41da63e8819ba9e77c890443830f","toolName":"read","startedAt":"2026-07-18T09:05:30.470Z","args":{"path":"crates/spt-term/src/reader.rs"},"intent":"Reading drain byte forwarding"},"id":"2a8ab50a","parentId":"2d8730f8","timestamp":"2026-07-18T09:05:30.470Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qXD8UacIVTfW1p0kJQKxKydm|fc_0ce5441d29b9f05d016a5b41da63f4819b9f4abae6313eb6ae","toolName":"grep","startedAt":"2026-07-18T09:05:30.470Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Finding spawn drain waiter"},"id":"1c39312c","parentId":"2a8ab50a","timestamp":"2026-07-18T09:05:30.470Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MRwo6mezkw7I3hy00XLhdqy6|fc_0ce5441d29b9f05d016a5b41da63fc819b9e0b89b9746c677b","toolName":"read","startedAt":"2026-07-18T09:05:30.470Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading session spawn teardown"},"id":"7a9e4684","parentId":"1c39312c","timestamp":"2026-07-18T09:05:30.470Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_tTGaAiMvee9vVmRKGD1sVVh9|fc_0ce5441d29b9f05d016a5b41da6404819bb9e59004d7ecd95e","toolName":"read","startedAt":"2026-07-18T09:05:30.470Z","args":{"path":"crates/spt-term/src/screen.rs"},"intent":"Reading screen mode tail"},"id":"f9a81cd8","parentId":"7a9e4684","timestamp":"2026-07-18T09:05:30.470Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_5gAp76dtR0vhv1trljJ4iKMv|fc_0ce5441d29b9f05d016a5b41da6408819bb88087d83c1b4f67","toolName":"grep","startedAt":"2026-07-18T09:05:30.470Z","args":{"path":"docs/KNOWN-HAZARDS.md; docs/adr; docs/design"},"intent":"Finding cleanup hazards"},"id":"bd42373f","parentId":"f9a81cd8","timestamp":"2026-07-18T09:05:30.470Z"}
{"type":"message","id":"26adb83f","parentId":"bd42373f","timestamp":"2026-07-18T09:05:30.472Z","message":{"role":"toolResult","toolCallId":"call_qXD8UacIVTfW1p0kJQKxKydm|fc_0ce5441d29b9f05d016a5b41da63f4819b9f4abae6313eb6ae","toolName":"grep","content":[{"type":"text","text":" 3517|    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n*3518|    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3519|        let req: SpawnReq =\n 3520|            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n*3521|        self.dispatch_spawn_policy(req, send, false)\n 3522|            .map(|id| id.expect(\"the ResumeOrReuse policy always yields a session\"))\n 3523|    }\n 3524|\n 3525|    /// The FRESH/RejectExisting policy (ADR-0041 decision 5,\n*3526|    /// REQ-SPAWN-FRESH-TRUTHFUL): same gate + spawn body as [`Self::dispatch_spawn`],\n 3527|    /// but an endpoint that already holds a live session answers the TYPED\n 3528|    /// [`KIND_SPAWN_CONFLICT`] — NEVER `Spawned(existing)`. `Ok(None)` = the\n 3529|    /// conflict was sent (no session to auto-subscribe).\n 3530|    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n*3531|    fn dispatch_spawn_fresh(&self, env: Envelope, send: &SharedSend) -> Result<Option<u64>, String> {\n 3532|        let req: SpawnReq =\n 3533|            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n*3534|        self.dispatch_spawn_policy(req, send, true)\n 3535|    }\n 3536|\n 3537|    /// The shared spawn choke behind BOTH policies (`fresh=false` =\n 3538|    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\n*3539|    fn dispatch_spawn_policy(\n 3540|        &self,\n 3541|        req: SpawnReq,\n 3542|        send: &SharedSend,\n...\n 3637|                                if Instant::now() >= reap_deadline {\n*3638|                                    let removed = recover(&self.sessions).remove(&sid);\n 3639|                                    if let Some(h) = removed {\n*3640|                                        recover_log(&h.log).stamp_reaped();\n 3641|                                        eprintln!(\n 3642|                                            \"SPAWN_ZOMBIE_FORCE_DETABLE:{}: exit-waiter never \\\n 3643|                                             reaped dead session {sid} — de-tabled directly\",\n...\n 3737|        let drain = session\n*3738|            .drain(move |chunk| {\n 3739|                // append() does the controller handoff inline now (one non-blocking\n 3740|                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n 3741|                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n...\n 3757|        thread::spawn(move || {\n*3758|            if let Ok(status) = waiter.wait() {\n*3759|                let ev = ExitEvent {\n 3760|                    session_id: id,\n 3761|                    code: Some(status.exit_code() as i32),\n 3762|                };\n...\n 3764|                    KIND_EXIT,\n*3765|                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\n 3766|                );\n 3767|                // Send to every attached sink — controller + all viewers (under\n 3768|                // the log lock).\n...\n 3782|            // about to vanish (the relatch the W2 int leg caught live).\n*3783|            recover(&sessions_exit).remove(&id);\n 3784|            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\n 3785|            // died, not the controller conn, so no detach path clears the perch\n 3786|            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\n...\n 3791|            // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\n*3792|            recover_log(&log_exit).stamp_reaped();\n 3793|        });\n 3794|\n 3795|        // Layer C: if the spawn carried a translation binary, bring it up with the\n...\n 3859|        //\n*3860|        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n 3861|        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n 3862|        // the per-session exit-waiter holds for the WHOLE life of a live child\n 3863|        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live"}],"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":15,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":15}],"truncated":false,"displayContent":" 3517│    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\n*3518│    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n 3519│        let req: SpawnReq =\n 3520│            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n*3521│        self.dispatch_spawn_policy(req, send, false)\n 3522│            .map(|id| id.expect(\"the ResumeOrReuse policy always yields a session\"))\n 3523│    }\n 3524│\n 3525│    /// The FRESH/RejectExisting policy (ADR-0041 decision 5,\n*3526│    /// REQ-SPAWN-FRESH-TRUTHFUL): same gate + spawn body as [`Self::dispatch_spawn`],\n 3527│    /// but an endpoint that already holds a live session answers the TYPED\n 3528│    /// [`KIND_SPAWN_CONFLICT`] — NEVER `Spawned(existing)`. `Ok(None)` = the\n 3529│    /// conflict was sent (no session to auto-subscribe).\n 3530│    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n*3531│    fn dispatch_spawn_fresh(&self, env: Envelope, send: &SharedSend) -> Result<Option<u64>, String> {\n 3532│        let req: SpawnReq =\n 3533│            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n*3534│        self.dispatch_spawn_policy(req, send, true)\n 3535│    }\n 3536│\n 3537│    /// The shared spawn choke behind BOTH policies (`fresh=false` =\n 3538│    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\n*3539│    fn dispatch_spawn_policy(\n 3540│        &self,\n 3541│        req: SpawnReq,\n 3542│        send: &SharedSend,\n     │...\n 3637│                                if Instant::now() >= reap_deadline {\n*3638│                                    let removed = recover(&self.sessions).remove(&sid);\n 3639│                                    if let Some(h) = removed {\n*3640│                                        recover_log(&h.log).stamp_reaped();\n 3641│                                        eprintln!(\n 3642│                                            \"SPAWN_ZOMBIE_FORCE_DETABLE:{}: exit-waiter never \\\n 3643│                                             reaped dead session {sid} — de-tabled directly\",\n     │...\n 3737│        let drain = session\n*3738│            .drain(move |chunk| {\n 3739│                // append() does the controller handoff inline now (one non-blocking\n 3740│                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n 3741│                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n     │...\n 3757│        thread::spawn(move || {\n*3758│            if let Ok(status) = waiter.wait() {\n*3759│                let ev = ExitEvent {\n 3760│                    session_id: id,\n 3761│                    code: Some(status.exit_code() as i32),\n 3762│                };\n     │...\n 3764│                    KIND_EXIT,\n*3765│                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\n 3766│                );\n 3767│                // Send to every attached sink — controller + all viewers (under\n 3768│                // the log lock).\n     │...\n 3782│            // about to vanish (the relatch the W2 int leg caught live).\n*3783│            recover(&sessions_exit).remove(&id);\n 3784│            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\n 3785│            // died, not the controller conn, so no detach path clears the perch\n 3786│            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\n     │...\n 3791│            // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\n*3792│            recover_log(&log_exit).stamp_reaped();\n 3793│        });\n 3794│\n 3795│        // Layer C: if the spawn carried a translation binary, bring it up with the\n     │...\n 3859│        //\n*3860│        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n 3861│        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n 3862│        // the per-session exit-waiter holds for the WHOLE life of a live child\n 3863│        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live"},"isError":false,"timestamp":1784365530472}}
{"type":"message","id":"23d6bf95","parentId":"26adb83f","timestamp":"2026-07-18T09:05:30.474Z","message":{"role":"toolResult","toolCallId":"call_KD52YdZEYAcDb8C0rRnrqXrC|fc_0ce5441d29b9f05d016a5b41da63e8819ba9e77c890443830f","toolName":"read","content":[{"type":"text","text":"//! Output drain pump — the ConPTY-DSR auto-answer reader (M3a A2).\n//!\n//! ConPTY can emit a cursor-position query `ESC [ 6 n` and **withhold all child\n//! stdout until the terminal answers it** (Spike #1: a run captured exactly the\n//! 4 bytes `\\e[6n` and zero program output — the child looked dead but was\n//! ticking fine; replying `ESC [ 1;1 R` immediately unblocked the full stream).\n//! Whether the query fires is build- and program-dependent (some ConPTY versions\n//! never emit it for short commands; an app that queries DSR itself always\n//! will). A real terminal *is* expected to answer DSR regardless, so the pump\n//! answers every `ESC[6n` it sees on every platform unconditionally — harmless\n//! correct CPR on the Unix raw-pipe backend, and the stall-breaker on any ConPTY\n//! that does withhold. The auto-answer is verified deterministically by injecting\n//! a synthetic DSR query (it must not depend on the host's ConPTY emitting one).\n//!\n//! **No-EOF-while-writer-held (Spike #1 secondary gotcha):** a ConPTY master\n//! does *not* EOF while the writer is held, so the read loop runs on its own\n//! thread and shutdown **never gates on a blocking `read()` returning**. The\n//! broker stops a surface by terminating the child (which tears the PTY down and\n//! unblocks the read); the [`Drain`] stop flag is a cooperative secondary signal\n//! observed whenever a read does return. Deterministic, ordered teardown (drop\n//! the writer, then the master) is the M3b broker's lifecycle responsibility.\n\nuse std::io::{Read, Write};\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\nuse std::sync::Arc;\nuse std::thread;\n\nuse crate::pty::{PtySession, SharedWriter};\nuse crate::surface::SurfaceError;\n\n/// The ConPTY startup cursor-position query (DSR — Device Status Report). All\n/// child stdout is withheld until this is answered (REQ-HAZARD-CONPTY-DSR).\nconst DSR_QUERY: &[u8] = b\"\\x1b[6n\";\n/// The reply: a Cursor Position Report placing the cursor at row 1, col 1. Any\n/// valid CPR unblocks ConPTY; `1;1` is the minimal answer Spike #1 proved.\nconst DSR_REPLY: &[u8] = b\"\\x1b[1;1R\";\n\n/// Streaming matcher for [`DSR_QUERY`] that survives chunk boundaries (the 4-byte\n/// query can split across two `read()`s). `DSR_QUERY` has no proper prefix that\n/// is also a suffix, so a single \"bytes matched so far\" counter is sufficient.\n#[derive(Default)]\nstruct DsrMatcher {\n    matched: usize,\n}\n\nimpl DsrMatcher {\n    /// Feed a chunk; return how many complete DSR queries it completed.\n    fn scan(&mut self, chunk: &[u8]) -> u64 {\n        let mut hits = 0;\n        for &b in chunk {\n            if b == DSR_QUERY[self.matched] {\n                self.matched += 1;\n                if self.matched == DSR_QUERY.len() {\n                    hits += 1;\n                    self.matched = 0;\n                }\n            } else {\n                // Restart — but `b` may itself begin a fresh match (e.g. a lone ESC).\n                self.matched = usize::from(b == DSR_QUERY[0]);\n            }\n        }\n        hits\n    }\n}\n\n/// A running drain pump over a surface's output. Holds live counters and a\n/// cooperative stop flag; see the module docs for the teardown contract.\npub struct Drain {\n    stop: Arc<AtomicBool>,\n    dsr_answered: Arc<AtomicU64>,\n    bytes: Arc<AtomicU64>,\n    handle: Option<thread::JoinHandle<()>>,\n}\n\nimpl Drain {\n    /// Total DSR queries answered so far (live).\n    pub fn dsr_answered(&self) -> u64 {\n        self.dsr_answered.load(Ordering::Relaxed)\n    }\n\n    /// Total bytes forwarded to the sink so far (live).\n    pub fn bytes_forwarded(&self) -> u64 {\n        self.bytes.load(Ordering::Relaxed)\n    }\n\n    /// Ask the pump to stop. Observed the next time a `read()` returns; it does\n    /// **not** interrupt a read already blocked (that requires tearing the child\n    /// down — the no-EOF-while-writer-held discipline).\n    pub fn request_stop(&self) {\n        self.stop.store(true, Ordering::Relaxed);\n    }\n\n    /// Join the pump thread. Only returns once the underlying `read()` has\n    /// unblocked (EOF / error / a returned read that observes the stop flag) —\n    /// on ConPTY that means the child must be torn down first. Best-effort.\n    pub fn join(mut self) {\n        if let Some(h) = self.handle.take() {\n            let _ = h.join();\n        }\n    }\n}\n\nimpl Drain {\n    /// Spawn the drain thread over `reader`, answering DSR via `writer` and\n    /// forwarding every byte to `sink`. The `sink` may block (that is how A4's\n    /// bounded channel applies backpressure).\n    fn spawn<R, S>(reader: R, writer: SharedWriter, mut sink: S) -> Self\n    where\n        R: Read + Send + 'static,\n        S: FnMut(&[u8]) + Send + 'static,\n    {\n        let stop = Arc::new(AtomicBool::new(false));\n        let dsr_answered = Arc::new(AtomicU64::new(0));\n        let bytes = Arc::new(AtomicU64::new(0));\n\n        let t_stop = Arc::clone(&stop);\n        let t_dsr = Arc::clone(&dsr_answered);\n        let t_bytes = Arc::clone(&bytes);\n\n        let handle = thread::spawn(move || {\n            let mut reader = reader;\n            let mut matcher = DsrMatcher::default();\n            let mut buf = [0u8; 8192];\n            loop {\n                if t_stop.load(Ordering::Relaxed) {\n                    break;\n                }\n                match reader.read(&mut buf) {\n                    Ok(0) => break, // EOF — child gone and handles released\n                    Ok(n) => {\n                        let chunk = &buf[..n];\n                        let hits = matcher.scan(chunk);\n                        if hits > 0 {\n                            // Answer through the shared, serialized writer so the\n                            // CPR never interleaves with caller-injected input.\n                            if let Ok(mut w) = writer.lock() {\n                                for _ in 0..hits {\n                                    let _ = w.write_all(DSR_REPLY);\n                                }\n                                let _ = w.flush();\n                            }\n                            t_dsr.fetch_add(hits, Ordering::Relaxed);\n                        }\n                        sink(chunk);\n                        t_bytes.fetch_add(n as u64, Ordering::Relaxed);\n                    }\n                    Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,\n                    Err(_) => break,\n                }\n            }\n        });\n\n        Drain {\n            stop,\n            dsr_answered,\n            bytes,\n            handle: Some(handle),\n        }\n    }\n}\n\nimpl PtySession {\n    /// Start draining this surface's output on a background thread, answering\n    /// ConPTY's DSR query and forwarding every byte to `sink`. Takes the master\n    /// reader (once — a second call returns [`SurfaceError::ReaderTaken`]).\n    ///\n    /// This is the substrate the A4 bounded byte-stream and the A5 digest ride.\n    //\n    // [impl->REQ-HAZARD-CONPTY-DSR]\n    pub fn drain<S>(&self, sink: S) -> Result<Drain, SurfaceError>\n    where\n        S: FnMut(&[u8]) + Send + 'static,\n    {\n        let reader = self.take_reader()?;\n        let writer = self.shared_writer();\n        Ok(Drain::spawn(reader, writer, sink))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::sync::Mutex;\n    use std::time::Duration;\n\n    // [unit->REQ-HAZARD-CONPTY-DSR]\n    #[test]\n    fn dsr_matcher_finds_query_across_chunk_boundary() {\n        let mut m = DsrMatcher::default();\n        // Split `\\x1b[6n` across two feeds.\n        assert_eq!(m.scan(b\"output\\x1b[\"), 0);\n        assert_eq!(m.scan(b\"6nmore\"), 1);\n        // A lone ESC that restarts a match mid-stream, then completes.\n        let mut m = DsrMatcher::default();\n        assert_eq!(m.scan(b\"\\x1b\\x1b[6n\"), 1);\n        // Two back-to-back queries.\n        let mut m = DsrMatcher::default();\n        assert_eq!(m.scan(b\"\\x1b[6n\\x1b[6n\"), 2);\n    }\n\n    /// A reader that yields one scripted chunk then EOFs — models a ConPTY\n    /// stream that emits a DSR query, independent of the host's real ConPTY.\n    struct ScriptedReader {\n        chunks: std::vec::IntoIter<Vec<u8>>,\n    }\n    impl Read for ScriptedReader {\n        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {\n            match self.chunks.next() {\n                Some(c) => {\n                    let n = c.len().min(buf.len());\n                    buf[..n].copy_from_slice(&c[..n]);\n                    Ok(n)\n                }\n                None => Ok(0),\n            }\n        }\n    }\n\n    /// A writer capturing everything written, behind the SharedWriter shape.\n    fn capturing_writer() -> (SharedWriter, Arc<Mutex<Vec<u8>>>) {\n        struct Cap(Arc<Mutex<Vec<u8>>>);\n        impl Write for Cap {\n            fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {\n…\n            }\n…\n        }\n…\n    }\n…\n}\n\n[Showing lines 1-223 of 287. Use :224 to continue]"}],"details":{"truncation":{"content":"//! Output drain pump — the ConPTY-DSR auto-answer reader (M3a A2).\r\n//!\r\n//! ConPTY can emit a cursor-position query `ESC [ 6 n` and **withhold all child\r\n//! stdout until the terminal answers it** (Spike #1: a run captured exactly the\r\n//! 4 bytes `\\e[6n` and zero program output — the child looked dead but was\r\n//! ticking fine; replying `ESC [ 1;1 R` immediately unblocked the full stream).\r\n//! Whether the query fires is build- and program-dependent (some ConPTY versions\r\n//! never emit it for short commands; an app that queries DSR itself always\r\n//! will). A real terminal *is* expected to answer DSR regardless, so the pump\r\n//! answers every `ESC[6n` it sees on every platform unconditionally — harmless\r\n//! correct CPR on the Unix raw-pipe backend, and the stall-breaker on any ConPTY\r\n//! that does withhold. The auto-answer is verified deterministically by injecting\r\n//! a synthetic DSR query (it must not depend on the host's ConPTY emitting one).\r\n//!\r\n//! **No-EOF-while-writer-held (Spike #1 secondary gotcha):** a ConPTY master\r\n//! does *not* EOF while the writer is held, so the read loop runs on its own\r\n//! thread and shutdown **never gates on a blocking `read()` returning**. The\r\n//! broker stops a surface by terminating the child (which tears the PTY down and\r\n//! unblocks the read); the [`Drain`] stop flag is a cooperative secondary signal\r\n//! observed whenever a read does return. Deterministic, ordered teardown (drop\r\n//! the writer, then the master) is the M3b broker's lifecycle responsibility.\r\n\r\nuse std::io::{Read, Write};\r\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\r\nuse std::sync::Arc;\r\nuse std::thread;\r\n\r\nuse crate::pty::{PtySession, SharedWriter};\r\nuse crate::surface::SurfaceError;\r\n\r\n/// The ConPTY startup cursor-position query (DSR — Device Status Report). All\r\n/// child stdout is withheld until this is answered (REQ-HAZARD-CONPTY-DSR).\r\nconst DSR_QUERY: &[u8] = b\"\\x1b[6n\";\r\n/// The reply: a Cursor Position Report placing the cursor at row 1, col 1. Any\r\n/// valid CPR unblocks ConPTY; `1;1` is the minimal answer Spike #1 proved.\r\nconst DSR_REPLY: &[u8] = b\"\\x1b[1;1R\";\r\n\r\n/// Streaming matcher for [`DSR_QUERY`] that survives chunk boundaries (the 4-byte\r\n/// query can split across two `read()`s). `DSR_QUERY` has no proper prefix that\r\n/// is also a suffix, so a single \"bytes matched so far\" counter is sufficient.\r\n#[derive(Default)]\r\nstruct DsrMatcher {\r\n    matched: usize,\r\n}\r\n\r\nimpl DsrMatcher {\r\n    /// Feed a chunk; return how many complete DSR queries it completed.\r\n    fn scan(&mut self, chunk: &[u8]) -> u64 {\r\n        let mut hits = 0;\r\n        for &b in chunk {\r\n            if b == DSR_QUERY[self.matched] {\r\n                self.matched += 1;\r\n                if self.matched == DSR_QUERY.len() {\r\n                    hits += 1;\r\n                    self.matched = 0;\r\n                }\r\n            } else {\r\n                // Restart — but `b` may itself begin a fresh match (e.g. a lone ESC).\r\n                self.matched = usize::from(b == DSR_QUERY[0]);\r\n            }\r\n        }\r\n        hits\r\n    }\r\n}\r\n\r\n/// A running drain pump over a surface's output. Holds live counters and a\r\n/// cooperative stop flag; see the module docs for the teardown contract.\r\npub struct Drain {\r\n    stop: Arc<AtomicBool>,\r\n    dsr_answered: Arc<AtomicU64>,\r\n    bytes: Arc<AtomicU64>,\r\n    handle: Option<thread::JoinHandle<()>>,\r\n}\r\n\r\nimpl Drain {\r\n    /// Total DSR queries answered so far (live).\r\n    pub fn dsr_answered(&self) -> u64 {\r\n        self.dsr_answered.load(Ordering::Relaxed)\r\n    }\r\n\r\n    /// Total bytes forwarded to the sink so far (live).\r\n    pub fn bytes_forwarded(&self) -> u64 {\r\n        self.bytes.load(Ordering::Relaxed)\r\n    }\r\n\r\n    /// Ask the pump to stop. Observed the next time a `read()` returns; it does\r\n    /// **not** interrupt a read already blocked (that requires tearing the child\r\n    /// down — the no-EOF-while-writer-held discipline).\r\n    pub fn request_stop(&self) {\r\n        self.stop.store(true, Ordering::Relaxed);\r\n    }\r\n\r\n    /// Join the pump thread. Only returns once the underlying `read()` has\r\n    /// unblocked (EOF / error / a returned read that observes the stop flag) —\r\n    /// on ConPTY that means the child must be torn down first. Best-effort.\r\n    pub fn join(mut self) {\r\n        if let Some(h) = self.handle.take() {\r\n            let _ = h.join();\r\n        }\r\n    }\r\n}\r\n\r\nimpl Drain {\r\n    /// Spawn the drain thread over `reader`, answering DSR via `writer` and\r\n    /// forwarding every byte to `sink`. The `sink` may block (that is how A4's\r\n    /// bounded channel applies backpressure).\r\n    fn spawn<R, S>(reader: R, writer: SharedWriter, mut sink: S) -> Self\r\n    where\r\n        R: Read + Send + 'static,\r\n        S: FnMut(&[u8]) + Send + 'static,\r\n    {\r\n        let stop = Arc::new(AtomicBool::new(false));\r\n        let dsr_answered = Arc::new(AtomicU64::new(0));\r\n        let bytes = Arc::new(AtomicU64::new(0));\r\n\r\n        let t_stop = Arc::clone(&stop);\r\n        let t_dsr = Arc::clone(&dsr_answered);\r\n        let t_bytes = Arc::clone(&bytes);\r\n\r\n        let handle = thread::spawn(move || {\r\n            let mut reader = reader;\r\n            let mut matcher = DsrMatcher::default();\r\n            let mut buf = [0u8; 8192];\r\n            loop {\r\n                if t_stop.load(Ordering::Relaxed) {\r\n                    break;\r\n                }\r\n                match reader.read(&mut buf) {\r\n                    Ok(0) => break, // EOF — child gone and handles released\r\n                    Ok(n) => {\r\n                        let chunk = &buf[..n];\r\n                        let hits = matcher.scan(chunk);\r\n                        if hits > 0 {\r\n                            // Answer through the shared, serialized writer so the\r\n                            // CPR never interleaves with caller-injected input.\r\n                            if let Ok(mut w) = writer.lock() {\r\n                                for _ in 0..hits {\r\n                                    let _ = w.write_all(DSR_REPLY);\r\n                                }\r\n                                let _ = w.flush();\r\n                            }\r\n                            t_dsr.fetch_add(hits, Ordering::Relaxed);\r\n                        }\r\n                        sink(chunk);\r\n                        t_bytes.fetch_add(n as u64, Ordering::Relaxed);\r\n                    }\r\n                    Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,\r\n                    Err(_) => break,\r\n                }\r\n            }\r\n        });\r\n\r\n        Drain {\r\n            stop,\r\n            dsr_answered,\r\n            bytes,\r\n            handle: Some(handle),\r\n        }\r\n    }\r\n}\r\n\r\nimpl PtySession {\r\n    /// Start draining this surface's output on a background thread, answering\r\n    /// ConPTY's DSR query and forwarding every byte to `sink`. Takes the master\r\n    /// reader (once — a second call returns [`SurfaceError::ReaderTaken`]).\r\n    ///\r\n    /// This is the substrate the A4 bounded byte-stream and the A5 digest ride.\r\n    //\r\n    // [impl->REQ-HAZARD-CONPTY-DSR]\r\n    pub fn drain<S>(&self, sink: S) -> Result<Drain, SurfaceError>\r\n    where\r\n        S: FnMut(&[u8]) + Send + 'static,\r\n    {\r\n        let reader = self.take_reader()?;\r\n        let writer = self.shared_writer();\r\n        Ok(Drain::spawn(reader, writer, sink))\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use std::sync::Mutex;\r\n    use std::time::Duration;\r\n\r\n    // [unit->REQ-HAZARD-CONPTY-DSR]\r\n    #[test]\r\n    fn dsr_matcher_finds_query_across_chunk_boundary() {\r\n        let mut m = DsrMatcher::default();\r\n        // Split `\\x1b[6n` across two feeds.\r\n        assert_eq!(m.scan(b\"output\\x1b[\"), 0);\r\n        assert_eq!(m.scan(b\"6nmore\"), 1);\r\n        // A lone ESC that restarts a match mid-stream, then completes.\r\n        let mut m = DsrMatcher::default();\r\n        assert_eq!(m.scan(b\"\\x1b\\x1b[6n\"), 1);\r\n        // Two back-to-back queries.\r\n        let mut m = DsrMatcher::default();\r\n        assert_eq!(m.scan(b\"\\x1b[6n\\x1b[6n\"), 2);\r\n    }\r\n\r\n    /// A reader that yields one scripted chunk then EOFs — models a ConPTY\r\n    /// stream that emits a DSR query, independent of the host's real ConPTY.\r\n    struct ScriptedReader {\r\n        chunks: std::vec::IntoIter<Vec<u8>>,\r\n    }\r\n    impl Read for ScriptedReader {\r\n        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {\r\n            match self.chunks.next() {\r\n                Some(c) => {\r\n                    let n = c.len().min(buf.len());\r\n                    buf[..n].copy_from_slice(&c[..n]);\r\n                    Ok(n)\r\n                }\r\n                None => Ok(0),\r\n            }\r\n        }\r\n    }\r\n\r\n    /// A writer capturing everything written, behind the SharedWriter shape.\r\n    fn capturing_writer() -> (SharedWriter, Arc<Mutex<Vec<u8>>>) {\r\n        struct Cap(Arc<Mutex<Vec<u8>>>);\r\n        impl Write for Cap {\r\n            fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {\r","truncated":true,"truncatedBy":"lines","totalLines":287,"totalBytes":9053,"outputLines":223,"outputBytes":9053,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Output drain pump — the ConPTY-DSR auto-answer reader (M3a A2).\r\n//!\r\n//! ConPTY can emit a cursor-position query `ESC [ 6 n` and **withhold all child\r\n//! stdout until the terminal answers it** (Spike #1: a run captured exactly the\r\n//! 4 bytes `\\e[6n` and zero program output — the child looked dead but was\r\n//! ticking fine; replying `ESC [ 1;1 R` immediately unblocked the full stream).\r\n//! Whether the query fires is build- and program-dependent (some ConPTY versions\r\n//! never emit it for short commands; an app that queries DSR itself always\r\n//! will). A real terminal *is* expected to answer DSR regardless, so the pump\r\n//! answers every `ESC[6n` it sees on every platform unconditionally — harmless\r\n//! correct CPR on the Unix raw-pipe backend, and the stall-breaker on any ConPTY\r\n//! that does withhold. The auto-answer is verified deterministically by injecting\r\n//! a synthetic DSR query (it must not depend on the host's ConPTY emitting one).\r\n//!\r\n//! **No-EOF-while-writer-held (Spike #1 secondary gotcha):** a ConPTY master\r\n//! does *not* EOF while the writer is held, so the read loop runs on its own\r\n//! thread and shutdown **never gates on a blocking `read()` returning**. The\r\n//! broker stops a surface by terminating the child (which tears the PTY down and\r\n//! unblocks the read); the [`Drain`] stop flag is a cooperative secondary signal\r\n//! observed whenever a read does return. Deterministic, ordered teardown (drop\r\n//! the writer, then the master) is the M3b broker's lifecycle responsibility.\r\n\r\nuse std::io::{Read, Write};\r\nuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering};\r\nuse std::sync::Arc;\r\nuse std::thread;\r\n\r\nuse crate::pty::{PtySession, SharedWriter};\r\nuse crate::surface::SurfaceError;\r\n\r\n/// The ConPTY startup cursor-position query (DSR — Device Status Report). All\r\n/// child stdout is withheld until this is answered (REQ-HAZARD-CONPTY-DSR).\r\nconst DSR_QUERY: &[u8] = b\"\\x1b[6n\";\r\n/// The reply: a Cursor Position Report placing the cursor at row 1, col 1. Any\r\n/// valid CPR unblocks ConPTY; `1;1` is the minimal answer Spike #1 proved.\r\nconst DSR_REPLY: &[u8] = b\"\\x1b[1;1R\";\r\n\r\n/// Streaming matcher for [`DSR_QUERY`] that survives chunk boundaries (the 4-byte\r\n/// query can split across two `read()`s). `DSR_QUERY` has no proper prefix that\r\n/// is also a suffix, so a single \"bytes matched so far\" counter is sufficient.\r\n#[derive(Default)]\r\nstruct DsrMatcher {\r\n    matched: usize,\r\n}\r\n\r\nimpl DsrMatcher {\r\n    /// Feed a chunk; return how many complete DSR queries it completed.\r\n    fn scan(&mut self, chunk: &[u8]) -> u64 {\r\n        let mut hits = 0;\r\n        for &b in chunk {\r\n            if b == DSR_QUERY[self.matched] {\r\n                self.matched += 1;\r\n                if self.matched == DSR_QUERY.len() {\r\n                    hits += 1;\r\n                    self.matched = 0;\r\n                }\r\n            } else {\r\n                // Restart — but `b` may itself begin a fresh match (e.g. a lone ESC).\r\n                self.matched = usize::from(b == DSR_QUERY[0]);\r\n            }\r\n        }\r\n        hits\r\n    }\r\n}\r\n\r\n/// A running drain pump over a surface's output. Holds live counters and a\r\n/// cooperative stop flag; see the module docs for the teardown contract.\r\npub struct Drain {\r\n    stop: Arc<AtomicBool>,\r\n    dsr_answered: Arc<AtomicU64>,\r\n    bytes: Arc<AtomicU64>,\r\n    handle: Option<thread::JoinHandle<()>>,\r\n}\r\n\r\nimpl Drain {\r\n    /// Total DSR queries answered so far (live).\r\n    pub fn dsr_answered(&self) -> u64 {\r\n        self.dsr_answered.load(Ordering::Relaxed)\r\n    }\r\n\r\n    /// Total bytes forwarded to the sink so far (live).\r\n    pub fn bytes_forwarded(&self) -> u64 {\r\n        self.bytes.load(Ordering::Relaxed)\r\n    }\r\n\r\n    /// Ask the pump to stop. Observed the next time a `read()` returns; it does\r\n    /// **not** interrupt a read already blocked (that requires tearing the child\r\n    /// down — the no-EOF-while-writer-held discipline).\r\n    pub fn request_stop(&self) {\r\n        self.stop.store(true, Ordering::Relaxed);\r\n    }\r\n\r\n    /// Join the pump thread. Only returns once the underlying `read()` has\r\n    /// unblocked (EOF / error / a returned read that observes the stop flag) —\r\n    /// on ConPTY that means the child must be torn down first. Best-effort.\r\n    pub fn join(mut self) {\r\n        if let Some(h) = self.handle.take() {\r\n            let _ = h.join();\r\n        }\r\n    }\r\n}\r\n\r\nimpl Drain {\r\n    /// Spawn the drain thread over `reader`, answering DSR via `writer` and\r\n    /// forwarding every byte to `sink`. The `sink` may block (that is how A4's\r\n    /// bounded channel applies backpressure).\r\n    fn spawn<R, S>(reader: R, writer: SharedWriter, mut sink: S) -> Self\r\n    where\r\n        R: Read + Send + 'static,\r\n        S: FnMut(&[u8]) + Send + 'static,\r\n    {\r\n        let stop = Arc::new(AtomicBool::new(false));\r\n        let dsr_answered = Arc::new(AtomicU64::new(0));\r\n        let bytes = Arc::new(AtomicU64::new(0));\r\n\r\n        let t_stop = Arc::clone(&stop);\r\n        let t_dsr = Arc::clone(&dsr_answered);\r\n        let t_bytes = Arc::clone(&bytes);\r\n\r\n        let handle = thread::spawn(move || {\r\n            let mut reader = reader;\r\n            let mut matcher = DsrMatcher::default();\r\n            let mut buf = [0u8; 8192];\r\n            loop {\r\n                if t_stop.load(Ordering::Relaxed) {\r\n                    break;\r\n                }\r\n                match reader.read(&mut buf) {\r\n                    Ok(0) => break, // EOF — child gone and handles released\r\n                    Ok(n) => {\r\n                        let chunk = &buf[..n];\r\n                        let hits = matcher.scan(chunk);\r\n                        if hits > 0 {\r\n                            // Answer through the shared, serialized writer so the\r\n                            // CPR never interleaves with caller-injected input.\r\n                            if let Ok(mut w) = writer.lock() {\r\n                                for _ in 0..hits {\r\n                                    let _ = w.write_all(DSR_REPLY);\r\n                                }\r\n                                let _ = w.flush();\r\n                            }\r\n                            t_dsr.fetch_add(hits, Ordering::Relaxed);\r\n                        }\r\n                        sink(chunk);\r\n                        t_bytes.fetch_add(n as u64, Ordering::Relaxed);\r\n                    }\r\n                    Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,\r\n                    Err(_) => break,\r\n                }\r\n            }\r\n        });\r\n\r\n        Drain {\r\n            stop,\r\n            dsr_answered,\r\n            bytes,\r\n            handle: Some(handle),\r\n        }\r\n    }\r\n}\r\n\r\nimpl PtySession {\r\n    /// Start draining this surface's output on a background thread, answering\r\n    /// ConPTY's DSR query and forwarding every byte to `sink`. Takes the master\r\n    /// reader (once — a second call returns [`SurfaceError::ReaderTaken`]).\r\n    ///\r\n    /// This is the substrate the A4 bounded byte-stream and the A5 digest ride.\r\n    //\r\n    // [impl->REQ-HAZARD-CONPTY-DSR]\r\n    pub fn drain<S>(&self, sink: S) -> Result<Drain, SurfaceError>\r\n    where\r\n        S: FnMut(&[u8]) + Send + 'static,\r\n    {\r\n        let reader = self.take_reader()?;\r\n        let writer = self.shared_writer();\r\n        Ok(Drain::spawn(reader, writer, sink))\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use std::sync::Mutex;\r\n    use std::time::Duration;\r\n\r\n    // [unit->REQ-HAZARD-CONPTY-DSR]\r\n    #[test]\r\n    fn dsr_matcher_finds_query_across_chunk_boundary() {\r\n        let mut m = DsrMatcher::default();\r\n        // Split `\\x1b[6n` across two feeds.\r\n        assert_eq!(m.scan(b\"output\\x1b[\"), 0);\r\n        assert_eq!(m.scan(b\"6nmore\"), 1);\r\n        // A lone ESC that restarts a match mid-stream, then completes.\r\n        let mut m = DsrMatcher::default();\r\n        assert_eq!(m.scan(b\"\\x1b\\x1b[6n\"), 1);\r\n        // Two back-to-back queries.\r\n        let mut m = DsrMatcher::default();\r\n        assert_eq!(m.scan(b\"\\x1b[6n\\x1b[6n\"), 2);\r\n    }\r\n\r\n    /// A reader that yields one scripted chunk then EOFs — models a ConPTY\r\n    /// stream that emits a DSR query, independent of the host's real ConPTY.\r\n    struct ScriptedReader {\r\n        chunks: std::vec::IntoIter<Vec<u8>>,\r\n    }\r\n    impl Read for ScriptedReader {\r\n        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {\r\n            match self.chunks.next() {\r\n                Some(c) => {\r\n                    let n = c.len().min(buf.len());\r\n                    buf[..n].copy_from_slice(&c[..n]);\r\n                    Ok(n)\r\n                }\r\n                None => Ok(0),\r\n            }\r\n        }\r\n    }\r\n\r\n    /// A writer capturing everything written, behind the SharedWriter shape.\r\n    fn capturing_writer() -> (SharedWriter, Arc<Mutex<Vec<u8>>>) {\r\n        struct Cap(Arc<Mutex<Vec<u8>>>);\r\n        impl Write for Cap {\r\n            fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {\r\n…\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,null,226,null,230,null,234,null,286]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\reader.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":287,"totalBytes":9053,"outputLines":223,"outputBytes":9053,"shownRange":{"start":1,"end":223},"nextOffset":224}}},"isError":false,"timestamp":1784365530474}}
{"type":"message","id":"0419e794","parentId":"23d6bf95","timestamp":"2026-07-18T09:05:30.481Z","message":{"role":"toolResult","toolCallId":"call_tTGaAiMvee9vVmRKGD1sVVh9|fc_0ce5441d29b9f05d016a5b41da6404819bb9e59004d7ecd95e","toolName":"read","content":[{"type":"text","text":"impl Perform for GridState {\n…\n    fn execute(&mut self, byte: u8) {\n        match byte {\n…\n            0x09 => {\n…\n            }\n            0x0A..=0x0C => {\n                // LF / VT / FF\n                self.pending_wrap = false;\n                self.line_feed();\n            }\n            0x0D => {\n                // CR\n                self.pending_wrap = false;\n                self.col = 0;\n            }\n            _ => {}\n        }\n    }\n\n    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\n        let private = intermediates.first() == Some(&b'?');\n        match action {\n            'A' => {\n                self.pending_wrap = false;\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\n            }\n            'B' | 'e' => {\n                self.pending_wrap = false;\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\n            }\n            'C' | 'a' => {\n                self.pending_wrap = false;\n                self.col = (self.col + ps(params, 0, 1) as usize).min(self.cols - 1);\n            }\n            'D' => {\n                self.pending_wrap = false;\n                self.col = self.col.saturating_sub(ps(params, 0, 1) as usize);\n            }\n            'E' => {\n                self.pending_wrap = false;\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\n                self.col = 0;\n            }\n            'F' => {\n                self.pending_wrap = false;\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\n                self.col = 0;\n            }\n            'G' | '`' => {\n                self.pending_wrap = false;\n                self.col = (ps(params, 0, 1) as usize - 1).min(self.cols - 1);\n            }\n            'd' => {\n                self.pending_wrap = false;\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\n            }\n            'H' | 'f' => {\n                self.pending_wrap = false;\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\n                self.col = (ps(params, 1, 1) as usize - 1).min(self.cols - 1);\n            }\n            'J' => self.erase_display(ps(params, 0, 0)),\n            'K' => self.erase_line(ps(params, 0, 0)),\n            'X' => self.erase_chars(ps(params, 0, 1) as usize),\n            '@' => self.insert_chars(ps(params, 0, 1) as usize),\n            'P' => self.delete_chars(ps(params, 0, 1) as usize),\n            'L' => self.insert_lines(ps(params, 0, 1) as usize),\n            'M' => self.delete_lines(ps(params, 0, 1) as usize),\n            'S' => self.scroll_up(ps(params, 0, 1) as usize),\n            'T' => self.scroll_down(ps(params, 0, 1) as usize),\n            'm' => self.apply_sgr(params),\n            'h' => self.set_mode(private, ps(params, 0, 0), true),\n            'l' => self.set_mode(private, ps(params, 0, 0), false),\n            'r' if !private => {\n                // DECSTBM — set the scroll region (1-based, inclusive).\n                let top = ps(params, 0, 1) as usize - 1;\n                let bottom = ps(params, 1, self.rows as u16) as usize - 1;\n                if top < bottom && bottom < self.rows {\n                    self.scroll_top = top;\n                    self.scroll_bottom = bottom;\n                    self.row = top;\n                    self.col = 0;\n                    self.pending_wrap = false;\n                }\n            }\n            _ => {}\n        }\n    }\n\n    // OSC 0 (icon+title) / OSC 2 (title): capture the child's window title so\n    // the repaint can replay it — vte's default drops OSC entirely, which is\n    // exactly the E-2 stale-title-on-cold-attach hole. Other OSC codes stay\n    // ignored (colour palette etc. are client-local).\n    // [impl->REQ-BROKER-SCREEN-GRID]\n    fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {\n        if let [code, rest @ ..] = params {\n            if code == b\"0\" || code == b\"2\" {\n                let title = rest\n                    .iter()\n                    .map(|p| String::from_utf8_lossy(p))\n                    .collect::<Vec<_>>()\n                    .join(\";\");\n                self.title = Some(title);\n            }\n        }\n    }\n\n    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {\n        // Ignore charset-designation escapes (intermediates present, e.g. ESC ( B).\n        if !intermediates.is_empty() {\n            return;\n        }\n        match byte {\n            b'c' => self.full_reset(),                 // RIS\n            b'7' => self.saved = Some((self.row, self.col)), // DECSC\n            b'8' => {\n                // DECRC\n                if let Some((r, c)) = self.saved {\n                    self.row = r.min(self.rows - 1);\n                    self.col = c.min(self.cols - 1);\n                    self.pending_wrap = false;\n                }\n            }\n            b'D' => {\n                // IND — index (down + scroll)\n                self.pending_wrap = false;\n                self.line_feed();\n            }\n            b'M' => {\n                // RI — reverse index (up + scroll)\n                self.pending_wrap = false;\n                self.reverse_line_feed();\n            }\n            b'E' => {\n                // NEL — next line\n                self.pending_wrap = false;\n                self.col = 0;\n                self.line_feed();\n            }\n            _ => {}\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn repaint(feed: &[&[u8]], rows: u16, cols: u16) -> String {\n        let mut g = ScreenGrid::new(rows, cols);\n        for chunk in feed {\n            g.advance(chunk);\n        }\n        String::from_utf8(g.render_repaint()).unwrap()\n    }\n\n    // A main-screen repaint carries the visible text, keeps the client on the main\n    // buffer (?1049l), and never enters the alt screen.\n    // [unit->REQ-BROKER-SCREEN-GRID]\n…\n}\n\n[Showing lines 719-873 of 1023. Use :874 to continue]"}],"details":{"truncation":{"content":"            }\r\n            0x0A..=0x0C => {\r\n                // LF / VT / FF\r\n                self.pending_wrap = false;\r\n                self.line_feed();\r\n            }\r\n            0x0D => {\r\n                // CR\r\n                self.pending_wrap = false;\r\n                self.col = 0;\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\r\n        let private = intermediates.first() == Some(&b'?');\r\n        match action {\r\n            'A' => {\r\n                self.pending_wrap = false;\r\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\r\n            }\r\n            'B' | 'e' => {\r\n                self.pending_wrap = false;\r\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\r\n            }\r\n            'C' | 'a' => {\r\n                self.pending_wrap = false;\r\n                self.col = (self.col + ps(params, 0, 1) as usize).min(self.cols - 1);\r\n            }\r\n            'D' => {\r\n                self.pending_wrap = false;\r\n                self.col = self.col.saturating_sub(ps(params, 0, 1) as usize);\r\n            }\r\n            'E' => {\r\n                self.pending_wrap = false;\r\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\r\n                self.col = 0;\r\n            }\r\n            'F' => {\r\n                self.pending_wrap = false;\r\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\r\n                self.col = 0;\r\n            }\r\n            'G' | '`' => {\r\n                self.pending_wrap = false;\r\n                self.col = (ps(params, 0, 1) as usize - 1).min(self.cols - 1);\r\n            }\r\n            'd' => {\r\n                self.pending_wrap = false;\r\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\r\n            }\r\n            'H' | 'f' => {\r\n                self.pending_wrap = false;\r\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\r\n                self.col = (ps(params, 1, 1) as usize - 1).min(self.cols - 1);\r\n            }\r\n            'J' => self.erase_display(ps(params, 0, 0)),\r\n            'K' => self.erase_line(ps(params, 0, 0)),\r\n            'X' => self.erase_chars(ps(params, 0, 1) as usize),\r\n            '@' => self.insert_chars(ps(params, 0, 1) as usize),\r\n            'P' => self.delete_chars(ps(params, 0, 1) as usize),\r\n            'L' => self.insert_lines(ps(params, 0, 1) as usize),\r\n            'M' => self.delete_lines(ps(params, 0, 1) as usize),\r\n            'S' => self.scroll_up(ps(params, 0, 1) as usize),\r\n            'T' => self.scroll_down(ps(params, 0, 1) as usize),\r\n            'm' => self.apply_sgr(params),\r\n            'h' => self.set_mode(private, ps(params, 0, 0), true),\r\n            'l' => self.set_mode(private, ps(params, 0, 0), false),\r\n            'r' if !private => {\r\n                // DECSTBM — set the scroll region (1-based, inclusive).\r\n                let top = ps(params, 0, 1) as usize - 1;\r\n                let bottom = ps(params, 1, self.rows as u16) as usize - 1;\r\n                if top < bottom && bottom < self.rows {\r\n                    self.scroll_top = top;\r\n                    self.scroll_bottom = bottom;\r\n                    self.row = top;\r\n                    self.col = 0;\r\n                    self.pending_wrap = false;\r\n                }\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    // OSC 0 (icon+title) / OSC 2 (title): capture the child's window title so\r\n    // the repaint can replay it — vte's default drops OSC entirely, which is\r\n    // exactly the E-2 stale-title-on-cold-attach hole. Other OSC codes stay\r\n    // ignored (colour palette etc. are client-local).\r\n    // [impl->REQ-BROKER-SCREEN-GRID]\r\n    fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {\r\n        if let [code, rest @ ..] = params {\r\n            if code == b\"0\" || code == b\"2\" {\r\n                let title = rest\r\n                    .iter()\r\n                    .map(|p| String::from_utf8_lossy(p))\r\n                    .collect::<Vec<_>>()\r\n                    .join(\";\");\r\n                self.title = Some(title);\r\n            }\r\n        }\r\n    }\r\n\r\n    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {\r\n        // Ignore charset-designation escapes (intermediates present, e.g. ESC ( B).\r\n        if !intermediates.is_empty() {\r\n            return;\r\n        }\r\n        match byte {\r\n            b'c' => self.full_reset(),                 // RIS\r\n            b'7' => self.saved = Some((self.row, self.col)), // DECSC\r\n            b'8' => {\r\n                // DECRC\r\n                if let Some((r, c)) = self.saved {\r\n                    self.row = r.min(self.rows - 1);\r\n                    self.col = c.min(self.cols - 1);\r\n                    self.pending_wrap = false;\r\n                }\r\n            }\r\n            b'D' => {\r\n                // IND — index (down + scroll)\r\n                self.pending_wrap = false;\r\n                self.line_feed();\r\n            }\r\n            b'M' => {\r\n                // RI — reverse index (up + scroll)\r\n                self.pending_wrap = false;\r\n                self.reverse_line_feed();\r\n            }\r\n            b'E' => {\r\n                // NEL — next line\r\n                self.pending_wrap = false;\r\n                self.col = 0;\r\n                self.line_feed();\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    fn repaint(feed: &[&[u8]], rows: u16, cols: u16) -> String {\r\n        let mut g = ScreenGrid::new(rows, cols);\r\n        for chunk in feed {\r\n            g.advance(chunk);\r\n        }\r\n        String::from_utf8(g.render_repaint()).unwrap()\r\n    }\r\n\r\n    // A main-screen repaint carries the visible text, keeps the client on the main\r\n    // buffer (?1049l), and never enters the alt screen.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r","truncated":true,"truncatedBy":"lines","totalLines":305,"totalBytes":6033,"outputLines":155,"outputBytes":6033,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"impl Perform for GridState {\n…\n    fn execute(&mut self, byte: u8) {\n        match byte {\n…\n            0x09 => {\n…\n            }\r\n            0x0A..=0x0C => {\r\n                // LF / VT / FF\r\n                self.pending_wrap = false;\r\n                self.line_feed();\r\n            }\r\n            0x0D => {\r\n                // CR\r\n                self.pending_wrap = false;\r\n                self.col = 0;\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {\r\n        let private = intermediates.first() == Some(&b'?');\r\n        match action {\r\n            'A' => {\r\n                self.pending_wrap = false;\r\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\r\n            }\r\n            'B' | 'e' => {\r\n                self.pending_wrap = false;\r\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\r\n            }\r\n            'C' | 'a' => {\r\n                self.pending_wrap = false;\r\n                self.col = (self.col + ps(params, 0, 1) as usize).min(self.cols - 1);\r\n            }\r\n            'D' => {\r\n                self.pending_wrap = false;\r\n                self.col = self.col.saturating_sub(ps(params, 0, 1) as usize);\r\n            }\r\n            'E' => {\r\n                self.pending_wrap = false;\r\n                self.row = (self.row + ps(params, 0, 1) as usize).min(self.rows - 1);\r\n                self.col = 0;\r\n            }\r\n            'F' => {\r\n                self.pending_wrap = false;\r\n                self.row = self.row.saturating_sub(ps(params, 0, 1) as usize);\r\n                self.col = 0;\r\n            }\r\n            'G' | '`' => {\r\n                self.pending_wrap = false;\r\n                self.col = (ps(params, 0, 1) as usize - 1).min(self.cols - 1);\r\n            }\r\n            'd' => {\r\n                self.pending_wrap = false;\r\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\r\n            }\r\n            'H' | 'f' => {\r\n                self.pending_wrap = false;\r\n                self.row = (ps(params, 0, 1) as usize - 1).min(self.rows - 1);\r\n                self.col = (ps(params, 1, 1) as usize - 1).min(self.cols - 1);\r\n            }\r\n            'J' => self.erase_display(ps(params, 0, 0)),\r\n            'K' => self.erase_line(ps(params, 0, 0)),\r\n            'X' => self.erase_chars(ps(params, 0, 1) as usize),\r\n            '@' => self.insert_chars(ps(params, 0, 1) as usize),\r\n            'P' => self.delete_chars(ps(params, 0, 1) as usize),\r\n            'L' => self.insert_lines(ps(params, 0, 1) as usize),\r\n            'M' => self.delete_lines(ps(params, 0, 1) as usize),\r\n            'S' => self.scroll_up(ps(params, 0, 1) as usize),\r\n            'T' => self.scroll_down(ps(params, 0, 1) as usize),\r\n            'm' => self.apply_sgr(params),\r\n            'h' => self.set_mode(private, ps(params, 0, 0), true),\r\n            'l' => self.set_mode(private, ps(params, 0, 0), false),\r\n            'r' if !private => {\r\n                // DECSTBM — set the scroll region (1-based, inclusive).\r\n                let top = ps(params, 0, 1) as usize - 1;\r\n                let bottom = ps(params, 1, self.rows as u16) as usize - 1;\r\n                if top < bottom && bottom < self.rows {\r\n                    self.scroll_top = top;\r\n                    self.scroll_bottom = bottom;\r\n                    self.row = top;\r\n                    self.col = 0;\r\n                    self.pending_wrap = false;\r\n                }\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    // OSC 0 (icon+title) / OSC 2 (title): capture the child's window title so\r\n    // the repaint can replay it — vte's default drops OSC entirely, which is\r\n    // exactly the E-2 stale-title-on-cold-attach hole. Other OSC codes stay\r\n    // ignored (colour palette etc. are client-local).\r\n    // [impl->REQ-BROKER-SCREEN-GRID]\r\n    fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {\r\n        if let [code, rest @ ..] = params {\r\n            if code == b\"0\" || code == b\"2\" {\r\n                let title = rest\r\n                    .iter()\r\n                    .map(|p| String::from_utf8_lossy(p))\r\n                    .collect::<Vec<_>>()\r\n                    .join(\";\");\r\n                self.title = Some(title);\r\n            }\r\n        }\r\n    }\r\n\r\n    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {\r\n        // Ignore charset-designation escapes (intermediates present, e.g. ESC ( B).\r\n        if !intermediates.is_empty() {\r\n            return;\r\n        }\r\n        match byte {\r\n            b'c' => self.full_reset(),                 // RIS\r\n            b'7' => self.saved = Some((self.row, self.col)), // DECSC\r\n            b'8' => {\r\n                // DECRC\r\n                if let Some((r, c)) = self.saved {\r\n                    self.row = r.min(self.rows - 1);\r\n                    self.col = c.min(self.cols - 1);\r\n                    self.pending_wrap = false;\r\n                }\r\n            }\r\n            b'D' => {\r\n                // IND — index (down + scroll)\r\n                self.pending_wrap = false;\r\n                self.line_feed();\r\n            }\r\n            b'M' => {\r\n                // RI — reverse index (up + scroll)\r\n                self.pending_wrap = false;\r\n                self.reverse_line_feed();\r\n            }\r\n            b'E' => {\r\n                // NEL — next line\r\n                self.pending_wrap = false;\r\n                self.col = 0;\r\n                self.line_feed();\r\n            }\r\n            _ => {}\r\n        }\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    fn repaint(feed: &[&[u8]], rows: u16, cols: u16) -> String {\r\n        let mut g = ScreenGrid::new(rows, cols);\r\n        for chunk in feed {\r\n            g.advance(chunk);\r\n        }\r\n        String::from_utf8(g.render_repaint()).unwrap()\r\n    }\r\n\r\n    // A main-screen repaint carries the visible text, keeps the client on the main\r\n    // buffer (?1049l), and never enters the alt screen.\r\n    // [unit->REQ-BROKER-SCREEN-GRID]\r\n…\n}","startLine":700,"lineNumbers":[700,null,705,706,null,714,null,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,null,1022]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-term\\src\\screen.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1023,"totalBytes":6033,"outputLines":155,"outputBytes":6033,"shownRange":{"start":719,"end":873},"nextOffset":874}}},"isError":false,"timestamp":1784365530481}}
{"type":"message","id":"f0c9e4a2","parentId":"0419e794","timestamp":"2026-07-18T09:05:30.526Z","message":{"role":"toolResult","toolCallId":"call_MRwo6mezkw7I3hy00XLhdqy6|fc_0ce5441d29b9f05d016a5b41da63fc819b9e0b89b9746c677b","toolName":"read","content":[{"type":"text","text":"    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n…\n        if let Some(host) = self.net.get() {\n…\n            for id in &my_cb_streams {\n                let _ = host.send_stream(*id, &[], true);\n                let _ = host.retire_stream_terminal(*id);\n                eprintln!(\"STREAM_CONNBOUND_RETIRE:{id}: opener conn exited — FIN + terminal retire\");\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        let req: SpawnReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n        self.dispatch_spawn_policy(req, send, false)\n            .map(|id| id.expect(\"the ResumeOrReuse policy always yields a session\"))\n    }\n\n    /// The FRESH/RejectExisting policy (ADR-0041 decision 5,\n    /// REQ-SPAWN-FRESH-TRUTHFUL): same gate + spawn body as [`Self::dispatch_spawn`],\n    /// but an endpoint that already holds a live session answers the TYPED\n    /// [`KIND_SPAWN_CONFLICT`] — NEVER `Spawned(existing)`. `Ok(None)` = the\n    /// conflict was sent (no session to auto-subscribe).\n    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n    fn dispatch_spawn_fresh(&self, env: Envelope, send: &SharedSend) -> Result<Option<u64>, String> {\n        let req: SpawnReq =\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\n        self.dispatch_spawn_policy(req, send, true)\n    }\n\n    /// The shared spawn choke behind BOTH policies (`fresh=false` =\n    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\n    fn dispatch_spawn_policy(\n        &self,\n        req: SpawnReq,\n        send: &SharedSend,\n        fresh: bool,\n    ) -> Result<Option<u64>, String> {\n        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\n        // sole spawner post-W3, so its own spawn choke is the single-flight authority.\n        // Dedup a duplicate wake/resume for an endpoint that already has a live session\n        // OR a spawn in flight — closing the check-then-spawn TOCTOU that let one wake\n        // spawn two identical launch trees (perri's flynn dup: duplicate perch writers\n        // then stomped info.json, routing injects to a contended record). The claim +\n        // the live-session check are read together under one atomic critical section so\n        // a concurrent spawn either sees our claim or its registered session — never a\n        // gap; NO I/O under the lock (process_id reads a cached pid; the ZOMBIE probe\n        // runs OFF the lock in the AlreadyLive arm). An empty endpoint is an\n        // ad-hoc/legacy spawn (no id to dedup on) → never gated. `_wake_claim` holds\n        // the claim to fn end, releasing (RAII) after the session is table-visible\n        // or on any early error.\n        // [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\n        let _wake_claim: Option<WakeClaimGuard> = if req.endpoint.is_empty() {\n            None\n        } else {\n            let deadline = Instant::now() + WAKE_DEDUP_WAIT;\n            loop {\n                enum Gate<'a> {\n                    Claimed(WakeClaimGuard<'a>),\n                    AlreadyLive {\n                        sid: u64,\n                        spid: Option<u32>,\n                        adapter_labeled: bool,\n                        spawned_ms_ago: u64,\n                    },\n                    Racing,\n                }\n                let gate = {\n                    let sessions = recover(&self.sessions);\n                    let mut inflight = recover(&self.wake_inflight);\n                    let live = sessions.iter().find(|(_, h)| h.endpoint == req.endpoint).map(\n                        |(sid, h)| {\n                            (\n                                *sid,\n                                h.session.process_id(),\n                                !h.adapter.is_empty(),\n                                h.spawned_at.elapsed().as_millis() as u64,\n                            )\n                        },\n                    );\n                    match wake_gate_decision(\n                        live.is_some(),\n                        inflight.contains(&req.endpoint),\n                        Instant::now() >= deadline,\n                    ) {\n                        WakeGate::AlreadyLive => {\n                            let (sid, spid, adapter_labeled, spawned_ms_ago) =\n                                live.expect(\"live is Some on AlreadyLive\");\n                            Gate::AlreadyLive { sid, spid, adapter_labeled, spawned_ms_ago }\n                        }\n                        WakeGate::Racing => Gate::Racing,\n                        WakeGate::Claim => {\n                            inflight.insert(req.endpoint.clone());\n                            Gate::Claimed(WakeClaimGuard {\n                                set: &self.wake_inflight,\n                                endpoint: req.endpoint.clone(),\n                            })\n                        }\n                    }\n                };\n                match gate {\n                    Gate::Claimed(guard) => break Some(guard),\n                    Gate::AlreadyLive { sid, spid, adapter_labeled, spawned_ms_ago } => {\n                        // ONE liveness authority (ADR-0041 decision 6,\n                        // REQ-ENDPOINT-CYCLE-HONEST): before refusing/deduping by\n                        // citing the claimed session, PROBE its client tree — off\n                        // every lock. A zombie (dead root, or an adapter wrapper\n                        // whose harness clients died past boot grace) is reaped\n                        // honestly — scoped tree-kill, then wait for the\n                        // exit-waiter to de-table it — and the gate re-evaluates:\n                        // the next pass claims and spawns for real. Never a\n                        // refusal citing a corpse.\n                        // [impl->REQ-ENDPOINT-CYCLE-HONEST]\n                        if session_is_zombie(spid, adapter_labeled, spawned_ms_ago) {\n                            eprintln!(\n                                \"SPAWN_ZOMBIE_REAP:{}: claimed session {sid} has a dead client \\\n                                 tree — reaping before honest respawn [REQ-ENDPOINT-CYCLE-HONEST]\",\n                                req.endpoint\n                            );\n                            if let Some(pid) = spid {\n                                spt_store::proc::kill_pid_tree(pid);\n                            }\n                            // Bounded wait for the exit-waiter to reap the table row;\n                            // belt: a wedged/missed waiter (root already dead) gets a\n                            // direct de-table + stamp clear so the gate can converge.\n                            let reap_deadline = Instant::now() + Duration::from_secs(5);\n                            loop {\n                                if !recover(&self.sessions).contains_key(&sid) {\n                                    break;\n                                }\n                                if Instant::now() >= reap_deadline {\n                                    let removed = recover(&self.sessions).remove(&sid);\n                                    if let Some(h) = removed {\n                                        recover_log(&h.log).stamp_reaped();\n                                        eprintln!(\n                                            \"SPAWN_ZOMBIE_FORCE_DETABLE:{}: exit-waiter never \\\n                                             reaped dead session {sid} — de-tabled directly\",\n                                            req.endpoint\n                                        );\n                                    }\n                                    break;\n                                }\n                                thread::sleep(WAKE_DEDUP_POLL);\n                            }\n                            continue; // re-evaluate the gate against the reaped truth\n                        }\n                        if fresh {\n                            // Fresh/RejectExisting: a GENUINELY live session is a\n                            // typed conflict — no session created, nothing mutated,\n                            // the live session untouched. Never Spawned(existing).\n                            // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\n                            eprintln!(\n                                \"SPAWN_FRESH_CONFLICT:{}: endpoint already has a live session \\\n                                 {sid} — typed conflict, no session created \\\n                                 [REQ-SPAWN-FRESH-TRUTHFUL]\",\n                                req.endpoint\n                            );\n                            let frame = Envelope::new(\n                                KIND_SPAWN_CONFLICT,\n                                serde_json::to_value(SpawnConflict {\n                                    endpoint: req.endpoint.clone(),\n                                    session_id: sid,\n                                })\n                                .expect(\"SpawnConflict serializes\"),\n                            );\n                            send_frame(send, &frame);\n                            return Ok(None);\n                        }\n                        eprintln!(\n                            \"SPAWN_DEDUP:{}: endpoint already has a live session {sid} — \\\n                             single-flight wake no-op ack [REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\",\n                            req.endpoint\n                        );\n                        let frame = Envelope::new(\n                            KIND_SPAWNED,\n                            serde_json::to_value(Spawned { session_id: sid, pid: spid })\n                                .expect(\"Spawned serializes\"),\n                        );\n                        send_frame(send, &frame);\n                        return Ok(Some(sid));\n                    }\n                    Gate::Racing => thread::sleep(WAKE_DEDUP_POLL),\n                }\n            }\n        };\n        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;\n        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).\n        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the\n        // inherited env — empty for a legacy/N-1 spawn (inherit only).\n        let session = PtySession::spawn_program_in_env(\n            &req.program,\n            &req.args,\n            SurfaceSize::new(req.rows, req.cols),\n            req.cwd.as_deref(),\n            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),\n        )\n        .map_err(|e| format!(\"spawn failed: {e}\"))?;\n        let session = Arc::new(session);\n        let id = self.next_id.fetch_add(1, Ordering::Relaxed);\n        let pid = session.process_id();\n\n        // The session's lone PTY input-writer (P0): the SOLE caller of the\n        // blocking `write_input`. Every dispatch-side write enqueues here, so a\n        // paste burst parks only this thread — never the broker dispatch thread\n        // (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The inject worker writes through it\n        // too, so the lone writer enforces the W2 inject-floor order.\n        let input = InputWriter::spawn(Arc::clone(&session), req.endpoint.clone());\n\n        // The output log, with the spawning connection pre-attached as the\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\n        // is missed before an explicit subscribe — and so a remote attach to a\n        // spawned-but-undriven session takes control silently (no loud displace\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\n        let log = Arc::new(Mutex::new(OutputLog::new(\n            id,\n            self.log_cap_chunks,\n            req.endpoint.clone(),\n            (req.rows, req.cols),\n        )));\n        log.lock()\n            .unwrap()\n            .become_controller(Arc::clone(send), None, 0, 0);\n\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\n        // then deliver to the controller OFF the lock with a bounded deadline\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\n        // longer inline under the log lock, so a backed-up controller can never\n        // wedge the session; a fully-stalled controller is detached after the\n        // deadline (bounded wedge, never permanent).\n        let log_drain = Arc::clone(&log);\n        let drain = session\n            .drain(move |chunk| {\n                // append() does the controller handoff inline now (one non-blocking\n                // try_send — b4) and returns Some(epoch) ONLY when the controller is\n                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\n                // the first lock (a fresh lock, no self-deadlock). The drain never\n                // sleeps here, so a slow controller can't throttle the viewer fan-out.\n                let evict = recover_log(&log_drain).append(chunk);\n                if let Some(epoch) = evict {\n                    recover_log(&log_drain).mark_controller_gone(epoch);\n                }\n            })\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\n\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\n        // session is removed from the table. Runs on its own thread; `wait`\n        // holds the child lock but `kill` uses a split killer.\n        let sessions_exit = Arc::clone(&self.sessions);\n        let log_exit = Arc::clone(&log);\n        let waiter = Arc::clone(&session);\n        thread::spawn(move || {\n            if let Ok(status) = waiter.wait() {\n                let ev = ExitEvent {\n                    session_id: id,\n                    code: Some(status.exit_code() as i32),\n                };\n                let frame = Envelope::new(\n                    KIND_EXIT,\n                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\n                );\n                // Send to every attached sink — controller + all viewers (under\n                // the log lock).\n                let sinks = recover_log(&log_exit).all_sinks();\n                for sink in sinks {\n                    // Bounded conn write (round-4): a wedged sink can no longer\n                    // park the exit-waiter thread indefinitely.\n                    let _ = sink.write(&frame);\n                }\n            }\n            // Reap the dead session from the table FIRST (ADR-0041 decision 4\n            // ordering): once removed, no NEW KIND_SESSIONS snapshot can see\n            // this session at all. Clearing before removal left a window where\n            // a poll snapshotted AFTER the generation bump — same generation,\n            // controller slot still occupied in the log — and legitimately\n            // re-wrote `controlled=true` onto the perch of a session that was\n            // about to vanish (the relatch the W2 int leg caught live).\n            recover(&sessions_exit).remove(&id);\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\n            // died, not the controller conn, so no detach path clears the perch\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\n            // outlive the session in info.json (or gossip a stale controller).\n            // Bumps the stamp generation under the per-endpoint write serial,\n            // so any snapshot taken BEFORE the removal is refused at write time.\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\n            // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\n            recover_log(&log_exit).stamp_reaped();\n        });\n\n        // Layer C: if the spawn carried a translation binary, bring it up with the\n        // session — spawn the pure stdin→stdout filter + the inject worker that\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\n        // aborting the session — a missing binary costs idle delivery, never the\n        // operator their agent.\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\n        // resets it on a healthy commit, the dispatch respawn path increments it).\n        let translation_respawns = Arc::new(AtomicU32::new(0));\n        let translation = req\n            .translation_binary\n            .as_deref()\n            .and_then(|argv| {\n                build_translation(\n                    argv,\n                    &req.endpoint,\n                    &input,\n                    Arc::clone(&translation_respawns),\n                    &log,\n                )\n            });\n\n        recover(&self.sessions).insert(\n            id,\n            HostedSession {\n                session,\n                input,\n                drain,\n                log,\n                endpoint: req.endpoint.clone(),\n                translation,\n                translation_argv: req.translation_binary.clone(),\n                translation_respawns,\n                adapter: req.adapter.clone(),\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\n                spawned_at: Instant::now(),\n            },\n        );\n\n        let resp = Spawned {\n            session_id: id,\n            pid,\n        };\n        let frame = Envelope::new(\n            KIND_SPAWNED,\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\n        );\n        send_frame(send, &frame);\n        Ok(Some(id))\n    }\n\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\n    /// self-kick guard.\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\n        let req: SubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\n        // Clone the log Arc out from under the sessions lock, then resolve under\n        // the log lock — never hold both locks at once.\n        //\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\n        // the per-session exit-waiter holds for the WHOLE life of a live child\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\n        // post-removal subscribe gets a prompt \"no such session\" error — both\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\n        // the rest.\n        let log = {\n            let sessions = recover(&self.sessions);\n            let h = sessions\n                .get(&req.session_id)\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\n            Arc::clone(&h.log)\n        };\n        let outcome = recover_log(&log).resolve_subscribe(\n            Arc::clone(send),\n            req.from_seq,\n            req.intent,\n            req.by,\n            req.gen,\n        );\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\n        Ok(req.session_id)\n    }\n\n    /// Release this connection's controller/viewer role on a session\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\n    /// a no-op if a newer controller already took over, or the session is gone.\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\n        let log = {\n            let sessions = recover(&self.sessions);\n            match sessions.get(&req.session_id) {\n                Some(h) => Arc::clone(&h.log),\n                None => {\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\n                    return Ok(());\n                }\n            }\n        };\n        recover_log(&log).detach_if_gen(send, req.gen);\n        Ok(())\n    }\n\n    /// Inject input bytes into a session's PTY.\n    ///\n    /// With an `op_id` the write is routed through the broker-owned\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\n\n[Showing lines 3499-3913 of 6771. Use :3914 to continue]"}],"details":{"truncation":{"content":"                let _ = host.send_stream(*id, &[], true);\r\n                let _ = host.retire_stream_terminal(*id);\r\n                eprintln!(\"STREAM_CONNBOUND_RETIRE:{id}: opener conn exited — FIN + terminal retire\");\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\r\n    /// exactly this isolation (one owner's hung shell binary stalls neither\r\n    /// another owner's delivery nor the broker's control dispatch).\r\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\r\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SpawnReq =\r\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\r\n        self.dispatch_spawn_policy(req, send, false)\r\n            .map(|id| id.expect(\"the ResumeOrReuse policy always yields a session\"))\r\n    }\r\n\r\n    /// The FRESH/RejectExisting policy (ADR-0041 decision 5,\r\n    /// REQ-SPAWN-FRESH-TRUTHFUL): same gate + spawn body as [`Self::dispatch_spawn`],\r\n    /// but an endpoint that already holds a live session answers the TYPED\r\n    /// [`KIND_SPAWN_CONFLICT`] — NEVER `Spawned(existing)`. `Ok(None)` = the\r\n    /// conflict was sent (no session to auto-subscribe).\r\n    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\r\n    fn dispatch_spawn_fresh(&self, env: Envelope, send: &SharedSend) -> Result<Option<u64>, String> {\r\n        let req: SpawnReq =\r\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\r\n        self.dispatch_spawn_policy(req, send, true)\r\n    }\r\n\r\n    /// The shared spawn choke behind BOTH policies (`fresh=false` =\r\n    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\r\n    fn dispatch_spawn_policy(\r\n        &self,\r\n        req: SpawnReq,\r\n        send: &SharedSend,\r\n        fresh: bool,\r\n    ) -> Result<Option<u64>, String> {\r\n        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\r\n        // sole spawner post-W3, so its own spawn choke is the single-flight authority.\r\n        // Dedup a duplicate wake/resume for an endpoint that already has a live session\r\n        // OR a spawn in flight — closing the check-then-spawn TOCTOU that let one wake\r\n        // spawn two identical launch trees (perri's flynn dup: duplicate perch writers\r\n        // then stomped info.json, routing injects to a contended record). The claim +\r\n        // the live-session check are read together under one atomic critical section so\r\n        // a concurrent spawn either sees our claim or its registered session — never a\r\n        // gap; NO I/O under the lock (process_id reads a cached pid; the ZOMBIE probe\r\n        // runs OFF the lock in the AlreadyLive arm). An empty endpoint is an\r\n        // ad-hoc/legacy spawn (no id to dedup on) → never gated. `_wake_claim` holds\r\n        // the claim to fn end, releasing (RAII) after the session is table-visible\r\n        // or on any early error.\r\n        // [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\r\n        let _wake_claim: Option<WakeClaimGuard> = if req.endpoint.is_empty() {\r\n            None\r\n        } else {\r\n            let deadline = Instant::now() + WAKE_DEDUP_WAIT;\r\n            loop {\r\n                enum Gate<'a> {\r\n                    Claimed(WakeClaimGuard<'a>),\r\n                    AlreadyLive {\r\n                        sid: u64,\r\n                        spid: Option<u32>,\r\n                        adapter_labeled: bool,\r\n                        spawned_ms_ago: u64,\r\n                    },\r\n                    Racing,\r\n                }\r\n                let gate = {\r\n                    let sessions = recover(&self.sessions);\r\n                    let mut inflight = recover(&self.wake_inflight);\r\n                    let live = sessions.iter().find(|(_, h)| h.endpoint == req.endpoint).map(\r\n                        |(sid, h)| {\r\n                            (\r\n                                *sid,\r\n                                h.session.process_id(),\r\n                                !h.adapter.is_empty(),\r\n                                h.spawned_at.elapsed().as_millis() as u64,\r\n                            )\r\n                        },\r\n                    );\r\n                    match wake_gate_decision(\r\n                        live.is_some(),\r\n                        inflight.contains(&req.endpoint),\r\n                        Instant::now() >= deadline,\r\n                    ) {\r\n                        WakeGate::AlreadyLive => {\r\n                            let (sid, spid, adapter_labeled, spawned_ms_ago) =\r\n                                live.expect(\"live is Some on AlreadyLive\");\r\n                            Gate::AlreadyLive { sid, spid, adapter_labeled, spawned_ms_ago }\r\n                        }\r\n                        WakeGate::Racing => Gate::Racing,\r\n                        WakeGate::Claim => {\r\n                            inflight.insert(req.endpoint.clone());\r\n                            Gate::Claimed(WakeClaimGuard {\r\n                                set: &self.wake_inflight,\r\n                                endpoint: req.endpoint.clone(),\r\n                            })\r\n                        }\r\n                    }\r\n                };\r\n                match gate {\r\n                    Gate::Claimed(guard) => break Some(guard),\r\n                    Gate::AlreadyLive { sid, spid, adapter_labeled, spawned_ms_ago } => {\r\n                        // ONE liveness authority (ADR-0041 decision 6,\r\n                        // REQ-ENDPOINT-CYCLE-HONEST): before refusing/deduping by\r\n                        // citing the claimed session, PROBE its client tree — off\r\n                        // every lock. A zombie (dead root, or an adapter wrapper\r\n                        // whose harness clients died past boot grace) is reaped\r\n                        // honestly — scoped tree-kill, then wait for the\r\n                        // exit-waiter to de-table it — and the gate re-evaluates:\r\n                        // the next pass claims and spawns for real. Never a\r\n                        // refusal citing a corpse.\r\n                        // [impl->REQ-ENDPOINT-CYCLE-HONEST]\r\n                        if session_is_zombie(spid, adapter_labeled, spawned_ms_ago) {\r\n                            eprintln!(\r\n                                \"SPAWN_ZOMBIE_REAP:{}: claimed session {sid} has a dead client \\\r\n                                 tree — reaping before honest respawn [REQ-ENDPOINT-CYCLE-HONEST]\",\r\n                                req.endpoint\r\n                            );\r\n                            if let Some(pid) = spid {\r\n                                spt_store::proc::kill_pid_tree(pid);\r\n                            }\r\n                            // Bounded wait for the exit-waiter to reap the table row;\r\n                            // belt: a wedged/missed waiter (root already dead) gets a\r\n                            // direct de-table + stamp clear so the gate can converge.\r\n                            let reap_deadline = Instant::now() + Duration::from_secs(5);\r\n                            loop {\r\n                                if !recover(&self.sessions).contains_key(&sid) {\r\n                                    break;\r\n                                }\r\n                                if Instant::now() >= reap_deadline {\r\n                                    let removed = recover(&self.sessions).remove(&sid);\r\n                                    if let Some(h) = removed {\r\n                                        recover_log(&h.log).stamp_reaped();\r\n                                        eprintln!(\r\n                                            \"SPAWN_ZOMBIE_FORCE_DETABLE:{}: exit-waiter never \\\r\n                                             reaped dead session {sid} — de-tabled directly\",\r\n                                            req.endpoint\r\n                                        );\r\n                                    }\r\n                                    break;\r\n                                }\r\n                                thread::sleep(WAKE_DEDUP_POLL);\r\n                            }\r\n                            continue; // re-evaluate the gate against the reaped truth\r\n                        }\r\n                        if fresh {\r\n                            // Fresh/RejectExisting: a GENUINELY live session is a\r\n                            // typed conflict — no session created, nothing mutated,\r\n                            // the live session untouched. Never Spawned(existing).\r\n                            // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\r\n                            eprintln!(\r\n                                \"SPAWN_FRESH_CONFLICT:{}: endpoint already has a live session \\\r\n                                 {sid} — typed conflict, no session created \\\r\n                                 [REQ-SPAWN-FRESH-TRUTHFUL]\",\r\n                                req.endpoint\r\n                            );\r\n                            let frame = Envelope::new(\r\n                                KIND_SPAWN_CONFLICT,\r\n                                serde_json::to_value(SpawnConflict {\r\n                                    endpoint: req.endpoint.clone(),\r\n                                    session_id: sid,\r\n                                })\r\n                                .expect(\"SpawnConflict serializes\"),\r\n                            );\r\n                            send_frame(send, &frame);\r\n                            return Ok(None);\r\n                        }\r\n                        eprintln!(\r\n                            \"SPAWN_DEDUP:{}: endpoint already has a live session {sid} — \\\r\n                             single-flight wake no-op ack [REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\",\r\n                            req.endpoint\r\n                        );\r\n                        let frame = Envelope::new(\r\n                            KIND_SPAWNED,\r\n                            serde_json::to_value(Spawned { session_id: sid, pid: spid })\r\n                                .expect(\"Spawned serializes\"),\r\n                        );\r\n                        send_frame(send, &frame);\r\n                        return Ok(Some(sid));\r\n                    }\r\n                    Gate::Racing => thread::sleep(WAKE_DEDUP_POLL),\r\n                }\r\n            }\r\n        };\r\n        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;\r\n        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).\r\n        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the\r\n        // inherited env — empty for a legacy/N-1 spawn (inherit only).\r\n        let session = PtySession::spawn_program_in_env(\r\n            &req.program,\r\n            &req.args,\r\n            SurfaceSize::new(req.rows, req.cols),\r\n            req.cwd.as_deref(),\r\n            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),\r\n        )\r\n        .map_err(|e| format!(\"spawn failed: {e}\"))?;\r\n        let session = Arc::new(session);\r\n        let id = self.next_id.fetch_add(1, Ordering::Relaxed);\r\n        let pid = session.process_id();\r\n\r\n        // The session's lone PTY input-writer (P0): the SOLE caller of the\r\n        // blocking `write_input`. Every dispatch-side write enqueues here, so a\r\n        // paste burst parks only this thread — never the broker dispatch thread\r\n        // (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The inject worker writes through it\r\n        // too, so the lone writer enforces the W2 inject-floor order.\r\n        let input = InputWriter::spawn(Arc::clone(&session), req.endpoint.clone());\r\n\r\n        // The output log, with the spawning connection pre-attached as the\r\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\r\n        // is missed before an explicit subscribe — and so a remote attach to a\r\n        // spawned-but-undriven session takes control silently (no loud displace\r\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\r\n        let log = Arc::new(Mutex::new(OutputLog::new(\r\n            id,\r\n            self.log_cap_chunks,\r\n            req.endpoint.clone(),\r\n            (req.rows, req.cols),\r\n        )));\r\n        log.lock()\r\n            .unwrap()\r\n            .become_controller(Arc::clone(send), None, 0, 0);\r\n\r\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\r\n        // then deliver to the controller OFF the lock with a bounded deadline\r\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\r\n        // longer inline under the log lock, so a backed-up controller can never\r\n        // wedge the session; a fully-stalled controller is detached after the\r\n        // deadline (bounded wedge, never permanent).\r\n        let log_drain = Arc::clone(&log);\r\n        let drain = session\r\n            .drain(move |chunk| {\r\n                // append() does the controller handoff inline now (one non-blocking\r\n                // try_send — b4) and returns Some(epoch) ONLY when the controller is\r\n                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\r\n                // the first lock (a fresh lock, no self-deadlock). The drain never\r\n                // sleeps here, so a slow controller can't throttle the viewer fan-out.\r\n                let evict = recover_log(&log_drain).append(chunk);\r\n                if let Some(epoch) = evict {\r\n                    recover_log(&log_drain).mark_controller_gone(epoch);\r\n                }\r\n            })\r\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\r\n\r\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\r\n        // session is removed from the table. Runs on its own thread; `wait`\r\n        // holds the child lock but `kill` uses a split killer.\r\n        let sessions_exit = Arc::clone(&self.sessions);\r\n        let log_exit = Arc::clone(&log);\r\n        let waiter = Arc::clone(&session);\r\n        thread::spawn(move || {\r\n            if let Ok(status) = waiter.wait() {\r\n                let ev = ExitEvent {\r\n                    session_id: id,\r\n                    code: Some(status.exit_code() as i32),\r\n                };\r\n                let frame = Envelope::new(\r\n                    KIND_EXIT,\r\n                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\r\n                );\r\n                // Send to every attached sink — controller + all viewers (under\r\n                // the log lock).\r\n                let sinks = recover_log(&log_exit).all_sinks();\r\n                for sink in sinks {\r\n                    // Bounded conn write (round-4): a wedged sink can no longer\r\n                    // park the exit-waiter thread indefinitely.\r\n                    let _ = sink.write(&frame);\r\n                }\r\n            }\r\n            // Reap the dead session from the table FIRST (ADR-0041 decision 4\r\n            // ordering): once removed, no NEW KIND_SESSIONS snapshot can see\r\n            // this session at all. Clearing before removal left a window where\r\n            // a poll snapshotted AFTER the generation bump — same generation,\r\n            // controller slot still occupied in the log — and legitimately\r\n            // re-wrote `controlled=true` onto the perch of a session that was\r\n            // about to vanish (the relatch the W2 int leg caught live).\r\n            recover(&sessions_exit).remove(&id);\r\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\r\n            // died, not the controller conn, so no detach path clears the perch\r\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\r\n            // outlive the session in info.json (or gossip a stale controller).\r\n            // Bumps the stamp generation under the per-endpoint write serial,\r\n            // so any snapshot taken BEFORE the removal is refused at write time.\r\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n            // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\n            recover_log(&log_exit).stamp_reaped();\r\n        });\r\n\r\n        // Layer C: if the spawn carried a translation binary, bring it up with the\r\n        // session — spawn the pure stdin→stdout filter + the inject worker that\r\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\r\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\r\n        // aborting the session — a missing binary costs idle delivery, never the\r\n        // operator their agent.\r\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\r\n        // resets it on a healthy commit, the dispatch respawn path increments it).\r\n        let translation_respawns = Arc::new(AtomicU32::new(0));\r\n        let translation = req\r\n            .translation_binary\r\n            .as_deref()\r\n            .and_then(|argv| {\r\n                build_translation(\r\n                    argv,\r\n                    &req.endpoint,\r\n                    &input,\r\n                    Arc::clone(&translation_respawns),\r\n                    &log,\r\n                )\r\n            });\r\n\r\n        recover(&self.sessions).insert(\r\n            id,\r\n            HostedSession {\r\n                session,\r\n                input,\r\n                drain,\r\n                log,\r\n                endpoint: req.endpoint.clone(),\r\n                translation,\r\n                translation_argv: req.translation_binary.clone(),\r\n                translation_respawns,\r\n                adapter: req.adapter.clone(),\r\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\r\n                spawned_at: Instant::now(),\r\n            },\r\n        );\r\n\r\n        let resp = Spawned {\r\n            session_id: id,\r\n            pid,\r\n        };\r\n        let frame = Envelope::new(\r\n            KIND_SPAWNED,\r\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(Some(id))\r\n    }\r\n\r\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\r\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\r\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\r\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\r\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\r\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\r\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\r\n    /// self-kick guard.\r\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\r\n        // Clone the log Arc out from under the sessions lock, then resolve under\r\n        // the log lock — never hold both locks at once.\r\n        //\r\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\r\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\r\n        // the per-session exit-waiter holds for the WHOLE life of a live child\r\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\r\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\r\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\r\n        // post-removal subscribe gets a prompt \"no such session\" error — both\r\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\r\n        // the rest.\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            let h = sessions\r\n                .get(&req.session_id)\r\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\r\n            Arc::clone(&h.log)\r\n        };\r\n        let outcome = recover_log(&log).resolve_subscribe(\r\n            Arc::clone(send),\r\n            req.from_seq,\r\n            req.intent,\r\n            req.by,\r\n            req.gen,\r\n        );\r\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\r\n        Ok(req.session_id)\r\n    }\r\n\r\n    /// Release this connection's controller/viewer role on a session\r\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\r\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\r\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\r\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\r\n    /// a no-op if a newer controller already took over, or the session is gone.\r\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&req.session_id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => {\r\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\r\n                    return Ok(());\r\n                }\r\n            }\r\n        };\r\n        recover_log(&log).detach_if_gen(send, req.gen);\r\n        Ok(())\r\n    }\r\n\r\n    /// Inject input bytes into a session's PTY.\r\n    ///\r\n    /// With an `op_id` the write is routed through the broker-owned\r\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\r","truncated":true,"truncatedBy":"lines","totalLines":3273,"totalBytes":22116,"outputLines":415,"outputBytes":22116,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {\n…\n        if let Some(host) = self.net.get() {\n…\n            for id in &my_cb_streams {\n                let _ = host.send_stream(*id, &[], true);\r\n                let _ = host.retire_stream_terminal(*id);\r\n                eprintln!(\"STREAM_CONNBOUND_RETIRE:{id}: opener conn exited — FIN + terminal retire\");\r\n            }\r\n            // And presence: the liveness log + its ring persist (D4c).\r\n            if my_presence_sub {\r\n                host.detach_presence_sub(&send);\r\n            }\r\n        }\r\n        Ok(())\r\n    }\r\n\r\n    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning\r\n    /// connection, start its output drain and exit waiter, and reply `spawned`.\r\n    /// Every hosted session gets its **own** drain + exit-waiter threads (KH\r\n    /// 7.4): a hung child blocks only its own threads — shell instances ride\r\n    /// exactly this isolation (one owner's hung shell binary stalls neither\r\n    /// another owner's delivery nor the broker's control dispatch).\r\n    // [impl->REQ-HAZARD-DAEMON-SCHED-NONBLOCKING]\r\n    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SpawnReq =\r\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\r\n        self.dispatch_spawn_policy(req, send, false)\r\n            .map(|id| id.expect(\"the ResumeOrReuse policy always yields a session\"))\r\n    }\r\n\r\n    /// The FRESH/RejectExisting policy (ADR-0041 decision 5,\r\n    /// REQ-SPAWN-FRESH-TRUTHFUL): same gate + spawn body as [`Self::dispatch_spawn`],\r\n    /// but an endpoint that already holds a live session answers the TYPED\r\n    /// [`KIND_SPAWN_CONFLICT`] — NEVER `Spawned(existing)`. `Ok(None)` = the\r\n    /// conflict was sent (no session to auto-subscribe).\r\n    // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\r\n    fn dispatch_spawn_fresh(&self, env: Envelope, send: &SharedSend) -> Result<Option<u64>, String> {\r\n        let req: SpawnReq =\r\n            serde_json::from_value(env.payload).map_err(|e| format!(\"bad spawn payload: {e}\"))?;\r\n        self.dispatch_spawn_policy(req, send, true)\r\n    }\r\n\r\n    /// The shared spawn choke behind BOTH policies (`fresh=false` =\r\n    /// ResumeOrReuse/wake, `fresh=true` = Fresh/RejectExisting).\r\n    fn dispatch_spawn_policy(\r\n        &self,\r\n        req: SpawnReq,\r\n        send: &SharedSend,\r\n        fresh: bool,\r\n    ) -> Result<Option<u64>, String> {\r\n        // Single-flight WAKE (W4, REQ-SPAWN-COLLISION-GUARD-LIVE-DUP): the broker is the\r\n        // sole spawner post-W3, so its own spawn choke is the single-flight authority.\r\n        // Dedup a duplicate wake/resume for an endpoint that already has a live session\r\n        // OR a spawn in flight — closing the check-then-spawn TOCTOU that let one wake\r\n        // spawn two identical launch trees (perri's flynn dup: duplicate perch writers\r\n        // then stomped info.json, routing injects to a contended record). The claim +\r\n        // the live-session check are read together under one atomic critical section so\r\n        // a concurrent spawn either sees our claim or its registered session — never a\r\n        // gap; NO I/O under the lock (process_id reads a cached pid; the ZOMBIE probe\r\n        // runs OFF the lock in the AlreadyLive arm). An empty endpoint is an\r\n        // ad-hoc/legacy spawn (no id to dedup on) → never gated. `_wake_claim` holds\r\n        // the claim to fn end, releasing (RAII) after the session is table-visible\r\n        // or on any early error.\r\n        // [impl->REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\r\n        let _wake_claim: Option<WakeClaimGuard> = if req.endpoint.is_empty() {\r\n            None\r\n        } else {\r\n            let deadline = Instant::now() + WAKE_DEDUP_WAIT;\r\n            loop {\r\n                enum Gate<'a> {\r\n                    Claimed(WakeClaimGuard<'a>),\r\n                    AlreadyLive {\r\n                        sid: u64,\r\n                        spid: Option<u32>,\r\n                        adapter_labeled: bool,\r\n                        spawned_ms_ago: u64,\r\n                    },\r\n                    Racing,\r\n                }\r\n                let gate = {\r\n                    let sessions = recover(&self.sessions);\r\n                    let mut inflight = recover(&self.wake_inflight);\r\n                    let live = sessions.iter().find(|(_, h)| h.endpoint == req.endpoint).map(\r\n                        |(sid, h)| {\r\n                            (\r\n                                *sid,\r\n                                h.session.process_id(),\r\n                                !h.adapter.is_empty(),\r\n                                h.spawned_at.elapsed().as_millis() as u64,\r\n                            )\r\n                        },\r\n                    );\r\n                    match wake_gate_decision(\r\n                        live.is_some(),\r\n                        inflight.contains(&req.endpoint),\r\n                        Instant::now() >= deadline,\r\n                    ) {\r\n                        WakeGate::AlreadyLive => {\r\n                            let (sid, spid, adapter_labeled, spawned_ms_ago) =\r\n                                live.expect(\"live is Some on AlreadyLive\");\r\n                            Gate::AlreadyLive { sid, spid, adapter_labeled, spawned_ms_ago }\r\n                        }\r\n                        WakeGate::Racing => Gate::Racing,\r\n                        WakeGate::Claim => {\r\n                            inflight.insert(req.endpoint.clone());\r\n                            Gate::Claimed(WakeClaimGuard {\r\n                                set: &self.wake_inflight,\r\n                                endpoint: req.endpoint.clone(),\r\n                            })\r\n                        }\r\n                    }\r\n                };\r\n                match gate {\r\n                    Gate::Claimed(guard) => break Some(guard),\r\n                    Gate::AlreadyLive { sid, spid, adapter_labeled, spawned_ms_ago } => {\r\n                        // ONE liveness authority (ADR-0041 decision 6,\r\n                        // REQ-ENDPOINT-CYCLE-HONEST): before refusing/deduping by\r\n                        // citing the claimed session, PROBE its client tree — off\r\n                        // every lock. A zombie (dead root, or an adapter wrapper\r\n                        // whose harness clients died past boot grace) is reaped\r\n                        // honestly — scoped tree-kill, then wait for the\r\n                        // exit-waiter to de-table it — and the gate re-evaluates:\r\n                        // the next pass claims and spawns for real. Never a\r\n                        // refusal citing a corpse.\r\n                        // [impl->REQ-ENDPOINT-CYCLE-HONEST]\r\n                        if session_is_zombie(spid, adapter_labeled, spawned_ms_ago) {\r\n                            eprintln!(\r\n                                \"SPAWN_ZOMBIE_REAP:{}: claimed session {sid} has a dead client \\\r\n                                 tree — reaping before honest respawn [REQ-ENDPOINT-CYCLE-HONEST]\",\r\n                                req.endpoint\r\n                            );\r\n                            if let Some(pid) = spid {\r\n                                spt_store::proc::kill_pid_tree(pid);\r\n                            }\r\n                            // Bounded wait for the exit-waiter to reap the table row;\r\n                            // belt: a wedged/missed waiter (root already dead) gets a\r\n                            // direct de-table + stamp clear so the gate can converge.\r\n                            let reap_deadline = Instant::now() + Duration::from_secs(5);\r\n                            loop {\r\n                                if !recover(&self.sessions).contains_key(&sid) {\r\n                                    break;\r\n                                }\r\n                                if Instant::now() >= reap_deadline {\r\n                                    let removed = recover(&self.sessions).remove(&sid);\r\n                                    if let Some(h) = removed {\r\n                                        recover_log(&h.log).stamp_reaped();\r\n                                        eprintln!(\r\n                                            \"SPAWN_ZOMBIE_FORCE_DETABLE:{}: exit-waiter never \\\r\n                                             reaped dead session {sid} — de-tabled directly\",\r\n                                            req.endpoint\r\n                                        );\r\n                                    }\r\n                                    break;\r\n                                }\r\n                                thread::sleep(WAKE_DEDUP_POLL);\r\n                            }\r\n                            continue; // re-evaluate the gate against the reaped truth\r\n                        }\r\n                        if fresh {\r\n                            // Fresh/RejectExisting: a GENUINELY live session is a\r\n                            // typed conflict — no session created, nothing mutated,\r\n                            // the live session untouched. Never Spawned(existing).\r\n                            // [impl->REQ-SPAWN-FRESH-TRUTHFUL]\r\n                            eprintln!(\r\n                                \"SPAWN_FRESH_CONFLICT:{}: endpoint already has a live session \\\r\n                                 {sid} — typed conflict, no session created \\\r\n                                 [REQ-SPAWN-FRESH-TRUTHFUL]\",\r\n                                req.endpoint\r\n                            );\r\n                            let frame = Envelope::new(\r\n                                KIND_SPAWN_CONFLICT,\r\n                                serde_json::to_value(SpawnConflict {\r\n                                    endpoint: req.endpoint.clone(),\r\n                                    session_id: sid,\r\n                                })\r\n                                .expect(\"SpawnConflict serializes\"),\r\n                            );\r\n                            send_frame(send, &frame);\r\n                            return Ok(None);\r\n                        }\r\n                        eprintln!(\r\n                            \"SPAWN_DEDUP:{}: endpoint already has a live session {sid} — \\\r\n                             single-flight wake no-op ack [REQ-SPAWN-COLLISION-GUARD-LIVE-DUP]\",\r\n                            req.endpoint\r\n                        );\r\n                        let frame = Envelope::new(\r\n                            KIND_SPAWNED,\r\n                            serde_json::to_value(Spawned { session_id: sid, pid: spid })\r\n                                .expect(\"Spawned serializes\"),\r\n                        );\r\n                        send_frame(send, &frame);\r\n                        return Ok(Some(sid));\r\n                    }\r\n                    Gate::Racing => thread::sleep(WAKE_DEDUP_POLL),\r\n                }\r\n            }\r\n        };\r\n        // REQ-HOST-RUN-2: spawn in the requested project cwd when carried;\r\n        // `None` inherits the broker's cwd (legacy/test + W1 bringup shortcut).\r\n        // Inject the manifest [env] vars (REQ-HAZARD-ENV-SUBST) on top of the\r\n        // inherited env — empty for a legacy/N-1 spawn (inherit only).\r\n        let session = PtySession::spawn_program_in_env(\r\n            &req.program,\r\n            &req.args,\r\n            SurfaceSize::new(req.rows, req.cols),\r\n            req.cwd.as_deref(),\r\n            req.env.iter().map(|(k, v)| (k.as_str(), v.as_str())),\r\n        )\r\n        .map_err(|e| format!(\"spawn failed: {e}\"))?;\r\n        let session = Arc::new(session);\r\n        let id = self.next_id.fetch_add(1, Ordering::Relaxed);\r\n        let pid = session.process_id();\r\n\r\n        // The session's lone PTY input-writer (P0): the SOLE caller of the\r\n        // blocking `write_input`. Every dispatch-side write enqueues here, so a\r\n        // paste burst parks only this thread — never the broker dispatch thread\r\n        // (REQ-HAZARD-PTY-INPUT-WRITER-WEDGE). The inject worker writes through it\r\n        // too, so the lone writer enforces the W2 inject-floor order.\r\n        let input = InputWriter::spawn(Arc::clone(&session), req.endpoint.clone());\r\n\r\n        // The output log, with the spawning connection pre-attached as the\r\n        // controller (identity `None` = LOCAL, not a remote driver) so no chunk\r\n        // is missed before an explicit subscribe — and so a remote attach to a\r\n        // spawned-but-undriven session takes control silently (no loud displace\r\n        // of the local-spawner placeholder; REQ-RCVIEW-1).\r\n        let log = Arc::new(Mutex::new(OutputLog::new(\r\n            id,\r\n            self.log_cap_chunks,\r\n            req.endpoint.clone(),\r\n            (req.rows, req.cols),\r\n        )));\r\n        log.lock()\r\n            .unwrap()\r\n            .become_controller(Arc::clone(send), None, 0, 0);\r\n\r\n        // Output drain → append under the log lock (fast: ring + viewer fan-out),\r\n        // then deliver to the controller OFF the lock with a bounded deadline\r\n        // (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). The controller write is no\r\n        // longer inline under the log lock, so a backed-up controller can never\r\n        // wedge the session; a fully-stalled controller is detached after the\r\n        // deadline (bounded wedge, never permanent).\r\n        let log_drain = Arc::clone(&log);\r\n        let drain = session\r\n            .drain(move |chunk| {\r\n                // append() does the controller handoff inline now (one non-blocking\r\n                // try_send — b4) and returns Some(epoch) ONLY when the controller is\r\n                // stalled-past-deadline / gone and must be evicted. mark it gone OFF\r\n                // the first lock (a fresh lock, no self-deadlock). The drain never\r\n                // sleeps here, so a slow controller can't throttle the viewer fan-out.\r\n                let evict = recover_log(&log_drain).append(chunk);\r\n                if let Some(epoch) = evict {\r\n                    recover_log(&log_drain).mark_controller_gone(epoch);\r\n                }\r\n            })\r\n            .map_err(|e| format!(\"drain failed: {e}\"))?;\r\n\r\n        // Exit waiter → one `exit` frame when the child is reaped, then the dead\r\n        // session is removed from the table. Runs on its own thread; `wait`\r\n        // holds the child lock but `kill` uses a split killer.\r\n        let sessions_exit = Arc::clone(&self.sessions);\r\n        let log_exit = Arc::clone(&log);\r\n        let waiter = Arc::clone(&session);\r\n        thread::spawn(move || {\r\n            if let Ok(status) = waiter.wait() {\r\n                let ev = ExitEvent {\r\n                    session_id: id,\r\n                    code: Some(status.exit_code() as i32),\r\n                };\r\n                let frame = Envelope::new(\r\n                    KIND_EXIT,\r\n                    serde_json::to_value(ev).expect(\"ExitEvent serializes\"),\r\n                );\r\n                // Send to every attached sink — controller + all viewers (under\r\n                // the log lock).\r\n                let sinks = recover_log(&log_exit).all_sinks();\r\n                for sink in sinks {\r\n                    // Bounded conn write (round-4): a wedged sink can no longer\r\n                    // park the exit-waiter thread indefinitely.\r\n                    let _ = sink.write(&frame);\r\n                }\r\n            }\r\n            // Reap the dead session from the table FIRST (ADR-0041 decision 4\r\n            // ordering): once removed, no NEW KIND_SESSIONS snapshot can see\r\n            // this session at all. Clearing before removal left a window where\r\n            // a poll snapshotted AFTER the generation bump — same generation,\r\n            // controller slot still occupied in the log — and legitimately\r\n            // re-wrote `controlled=true` onto the perch of a session that was\r\n            // about to vanish (the relatch the W2 int leg caught live).\r\n            recover(&sessions_exit).remove(&id);\r\n            // Reap-time unlatch (REQ-HAZARD-CONTROL-STAMP-LIFETIME): the child\r\n            // died, not the controller conn, so no detach path clears the perch\r\n            // — do it here so ONLINE+CONTROLLED / driven_by / viewer_count cannot\r\n            // outlive the session in info.json (or gossip a stale controller).\r\n            // Bumps the stamp generation under the per-endpoint write serial,\r\n            // so any snapshot taken BEFORE the removal is refused at write time.\r\n            // [impl->REQ-HAZARD-CONTROL-STAMP-LIFETIME]\r\n            // [impl->REQ-CONTROL-STAMP-CONVERGENCE]\r\n            recover_log(&log_exit).stamp_reaped();\r\n        });\r\n\r\n        // Layer C: if the spawn carried a translation binary, bring it up with the\r\n        // session — spawn the pure stdin→stdout filter + the inject worker that\r\n        // owns the atomic PTY apply. A spawn failure FAILS CLOSED to SPOOLING\r\n        // (logged, `None` → idle inbound spools, never raw-injected) rather than\r\n        // aborting the session — a missing binary costs idle delivery, never the\r\n        // operator their agent.\r\n        // C-1: the shared bounded-respawn give-up counter (starts at 0; the worker\r\n        // resets it on a healthy commit, the dispatch respawn path increments it).\r\n        let translation_respawns = Arc::new(AtomicU32::new(0));\r\n        let translation = req\r\n            .translation_binary\r\n            .as_deref()\r\n            .and_then(|argv| {\r\n                build_translation(\r\n                    argv,\r\n                    &req.endpoint,\r\n                    &input,\r\n                    Arc::clone(&translation_respawns),\r\n                    &log,\r\n                )\r\n            });\r\n\r\n        recover(&self.sessions).insert(\r\n            id,\r\n            HostedSession {\r\n                session,\r\n                input,\r\n                drain,\r\n                log,\r\n                endpoint: req.endpoint.clone(),\r\n                translation,\r\n                translation_argv: req.translation_binary.clone(),\r\n                translation_respawns,\r\n                adapter: req.adapter.clone(),\r\n                install_dir: req.install_dir.clone().map(std::path::PathBuf::from),\r\n                spawned_at: Instant::now(),\r\n            },\r\n        );\r\n\r\n        let resp = Spawned {\r\n            session_id: id,\r\n            pid,\r\n        };\r\n        let frame = Envelope::new(\r\n            KIND_SPAWNED,\r\n            serde_json::to_value(resp).expect(\"Spawned serializes\"),\r\n        );\r\n        send_frame(send, &frame);\r\n        Ok(Some(id))\r\n    }\r\n\r\n    /// Re-attach a brain to a session's output under the W2.5 controller/viewer\r\n    /// model (REQ-RCVIEW-1 / REQ-KICK-1), replaying from `from_seq` for gapless,\r\n    /// exactly-once resume. The `intent` + `by` select the role; the broker\r\n    /// resolves it (controller take / viewer add / busy-refuse / loud displace)\r\n    /// and replies the [`SubscribeOutcome`] so the serving brain can wake\r\n    /// (controller-only), stay wake-neutral (viewer), or refuse (busy). A\r\n    /// same-`by` re-subscribe silently re-takes the controller — the gate-#7\r\n    /// self-kick guard.\r\n    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {\r\n        let req: SubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad subscribe payload: {e}\"))?;\r\n        // Clone the log Arc out from under the sessions lock, then resolve under\r\n        // the log lock — never hold both locks at once.\r\n        //\r\n        // NOTE (REQ-HAZARD-RC-ATTACH-FAILFAST): a `session.try_wait()` dead-child\r\n        // fast-detect MUST NOT live here — `try_wait` locks the same child mutex\r\n        // the per-session exit-waiter holds for the WHOLE life of a live child\r\n        // (`PtySession::wait`, broker.rs exit-waiter), so it deadlocks every live\r\n        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter\r\n        // already broadcasts an `Exit` frame to all attached sinks on reap, and a\r\n        // post-removal subscribe gets a prompt \"no such session\" error — both\r\n        // non-silent. The rc-side status-gate (a) + first-event backstop (b) cover\r\n        // the rest.\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            let h = sessions\r\n                .get(&req.session_id)\r\n                .ok_or_else(|| format!(\"no such session {}\", req.session_id))?;\r\n            Arc::clone(&h.log)\r\n        };\r\n        let outcome = recover_log(&log).resolve_subscribe(\r\n            Arc::clone(send),\r\n            req.from_seq,\r\n            req.intent,\r\n            req.by,\r\n            req.gen,\r\n        );\r\n        send_frame(send, &subscribed_envelope(req.session_id, outcome));\r\n        Ok(req.session_id)\r\n    }\r\n\r\n    /// Release this connection's controller/viewer role on a session\r\n    /// (REQ-RCVIEW-1): clears the controller slot iff this is the controller\r\n    /// (re-stamping `driven_by`) and removes any viewer rows it owns\r\n    /// (re-stamping `viewer_count`). The explicit detach a `serve_attach` worker\r\n    /// sends when its viewport ends, so the markers clear promptly. Idempotent —\r\n    /// a no-op if a newer controller already took over, or the session is gone.\r\n    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\r\n        let req: UnsubscribeReq = serde_json::from_value(env.payload)\r\n            .map_err(|e| format!(\"bad unsubscribe payload: {e}\"))?;\r\n        let log = {\r\n            let sessions = recover(&self.sessions);\r\n            match sessions.get(&req.session_id) {\r\n                Some(h) => Arc::clone(&h.log),\r\n                None => {\r\n                    eprintln!(\"DBG unsubscribe: no session {}\", req.session_id);\r\n                    return Ok(());\r\n                }\r\n            }\r\n        };\r\n        recover_log(&log).detach_if_gen(send, req.gen);\r\n        Ok(())\r\n    }\r\n\r\n    /// Inject input bytes into a session's PTY.\r\n    ///\r\n    /// With an `op_id` the write is routed through the broker-owned\r\n    /// [`EffectJournal`] for exactly-once delivery: a replay of the same\r","startLine":3153,"lineNumbers":[3153,null,3481,null,3498,3499,3500,3501,3502,3503,3504,3505,3506,3507,3508,3509,3510,3511,3512,3513,3514,3515,3516,3517,3518,3519,3520,3521,3522,3523,3524,3525,3526,3527,3528,3529,3530,3531,3532,3533,3534,3535,3536,3537,3538,3539,3540,3541,3542,3543,3544,3545,3546,3547,3548,3549,3550,3551,3552,3553,3554,3555,3556,3557,3558,3559,3560,3561,3562,3563,3564,3565,3566,3567,3568,3569,3570,3571,3572,3573,3574,3575,3576,3577,3578,3579,3580,3581,3582,3583,3584,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,3643,3644,3645,3646,3647,3648,3649,3650,3651,3652,3653,3654,3655,3656,3657,3658,3659,3660,3661,3662,3663,3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3674,3675,3676,3677,3678,3679,3680,3681,3682,3683,3684,3685,3686,3687,3688,3689,3690,3691,3692,3693,3694,3695,3696,3697,3698,3699,3700,3701,3702,3703,3704,3705,3706,3707,3708,3709,3710,3711,3712,3713,3714,3715,3716,3717,3718,3719,3720,3721,3722,3723,3724,3725,3726,3727,3728,3729,3730,3731,3732,3733,3734,3735,3736,3737,3738,3739,3740,3741,3742,3743,3744,3745,3746,3747,3748,3749,3750,3751,3752,3753,3754,3755,3756,3757,3758,3759,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6771,"totalBytes":22116,"outputLines":415,"outputBytes":22116,"shownRange":{"start":3499,"end":3913},"nextOffset":3914}}},"isError":false,"timestamp":1784365530526}}
{"type":"message","id":"e7bf7e57","parentId":"f0c9e4a2","timestamp":"2026-07-18T09:05:30.533Z","message":{"role":"toolResult","toolCallId":"call_5gAp76dtR0vhv1trljJ4iKMv|fc_0ce5441d29b9f05d016a5b41da6408819bb88087d83c1b4f67","toolName":"grep","content":[{"type":"text","text":"# docs/\n## KNOWN-HAZARDS.md\n 79|\n*80|### 3.1 Ephemeral perch cleanup on every `ring` exit path\n*81|- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\n 82|- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.\n*83|- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.\n 84|- **Sister cite:** `src/owl/ring.rs:58-294`.\n 85|\n 86|### 3.2 Stale signoff sentinel must not kill a fresh start\n...\n 89|- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\n*90|- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\n 91|\n 92|### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\n 93|- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\n...\n 114|\n*115|### 4.3 Registry stale-entry cleanup precedes lookup\n 116|- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n 117|- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n 118|- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n...\n 273|- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `Bind...\n*274|- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind t...\n 275|- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n 276|- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\n 277|\n...\n 294|\n*295|### 6.2 Soft-cleanup preserves state, removes `ready`\n*296|- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\n 297|- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\n 298|- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\n 299|- **Sister cite:** `src/owl/stop.rs`.\n...\n 555|\n*556|<!-- [doc->REQ-HAZARD-CONTROL-STAMP-LIFETIME] -->\n*557|### 7.27 A control/viewer stamp must NEVER outlive its session — every teardown path clears what attach stamped  `[REQ-HAZARD-CONTROL-STAMP-LIFETIME]`\n 558|- **Failure (F-026 #2, live evidence HFENDULEAM):** an spt-hosted endpoint stayed `ONLINE+CONTROLLED` after the operator's RC `/exit` — hours later hall-a's `info.json` still read `controlled:true` (status offline, dormant) and hfenduleam still GOSSIPED `controller_node=self` cross-node (SPT_DEV.json snapshot). ROOT: `/exit` kills the CHILD, not the controller connection, so the endpoint suspends via child termination and the teardown runs the REAP path (`broker.rs` exit-waiter → `sessions.remove(&id...\n 559|- **Invariant:** on session reap the broker clears the perch's control/viewer stamps — `OutputLog::stamp_reaped()` = `set_driven_by(None)` + `set_controlled(false)` + `set_viewer_count(0)` via the known endpoint id — so no teardown path leaves a stamp behind. The broker stays the SINGLE writer; the clear is unconditional (idempotent when already clear) and RACE-FREE — the session is dead, so there is no live controller to re-stamp concurrently (the same argument as 7.15's no-session leg). Best-effo...\n 560|- **spt-core mapping:** impl `broker.rs` `OutputLog::stamp_reaped` + the exit-waiter reap call (before `sessions.remove`); int (real broker+PTY, no mocks) `crates/spt-daemon/tests/control_stamp_lifetime.rs` `reap_clears_control_and_viewer_stamps` (spawn hosted session → the live controller stamps `controlled=true` → latch `driven_by`+`viewer_count` on disk → KILL the child → the reap clears all three; RED-first verified: fix disabled leaves `driven_by=Some`, `controlled=true`, `viewer_count=Some(...\n...\n 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...\n 698|<!-- [doc->REQ-HAZARD-ENDPOINT-LIFECYCLE] -->\n*699|- **Failure (paid-for, three hertz reports + operator field 2026-07-16):** four families, one root shape — lifecycle state written by multiple non-converging paths, optimistic stamps never verified. (a) `cmd_listen` stamped `status=online` from manifest capability alone → dead-PID hybrid rows survived EVERY restart (reconcile skipped them by state, cleanup gated on `controllable=true`, readers trusted `status=online` past dead-PID rejection). (b) Definitive session death stamped `status=offline` only...\n*700|- **Invariant:** online is stamped only from persisted state + hosting authority; legacy rows self-heal after a SUCCESSFUL broker query (broker failure ≠ empty set); control-stamp cleanup runs for every endpoint absent from session truth regardless of state/controllability, split from offline classification; definitive death = ONE atomic store mutation (offline + rest_state=suspended + clear dormant_since_ms, same info.json write) — never a rest-event edge, never reader-side inference; stamp writes a...\n 701|- **spt-core mapping:** `spt/src/api/startup.rs` `cmd_listen` creator gate; `spt-daemon/src/livehost.rs` reconcile split + decide_resume; `spt-daemon/src/lifecycle.rs` `mark_offline` → terminal-normalize; `spt/src/cli.rs` `cmd_stop`/create dispatch; `spt-daemon/src/broker.rs` SpawnReq policy split + KIND_SPAWN_FRESH + dup-guard probe; KH 7.12/7.27/7.29 kin.\n 702|- **Source:** REGISTRY-LIFECYCLE W2/W3 (hertz emphasys triple + rest-normalize + spawn-fresh reports 2026-07-16, doyle seam-verified same day; ADR-0041; operator deployah-wedge field evidence).\n 703|\n...\n 714|| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |\n*715|| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |\n 716|| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |\n 717|| 4.1 | Envelope decode order, `&amp;` last | spt-proto (public, wire-versioned) |\n 718|| 4.2 | Parser panic-free + tolerant | spt-proto |\n\n## adr/\n### 0041-endpoint-lifecycle-truth.md\n 17|   minting `ready_agent`/`controllable=false` hybrid rows born online-authoritative. The\n*18|   livehost reconcile skips `state != live_agent` rows AND gates cleanup on\n 19|   `controllable=true`, so a dead-PID hybrid survives every restart; `is_perch_alive`\n 20|   trusts `status=online` and skips dead-PID rejection — every reader faithfully\n 21|   resurrects the lie.\n...\n 45|   failure is never interpreted as an empty session set (no mass-offline on a hiccup).\n*46|2. **Control cleanup splits from offline classification.** Reconcile clears\n 47|   `controlled`/`driven_by`/`viewer_count` for EVERY endpoint absent from session truth —\n 48|   regardless of state or controllability — while offline classification keeps its narrow\n 49|   gate (live_agent + controllable). A row's classification quirk can never shield stale\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"docs/KNOWN-HAZARDS.md, docs/adr, docs/design","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":16,"fileCount":2,"files":["docs/KNOWN-HAZARDS.md","docs/adr/0041-endpoint-lifecycle-truth.md"],"fileMatches":[{"path":"docs/KNOWN-HAZARDS.md","count":14},{"path":"docs/adr/0041-endpoint-lifecycle-truth.md","count":2}],"truncated":true,"displayContent":"# docs/\n## KNOWN-HAZARDS.md\n  79│\n *80│### 3.1 Ephemeral perch cleanup on every `ring` exit path\n *81│- **Failure:** `ring` creates an ephemeral perch; early-exit paths (no-perch, empty-msg, timeout) skip cleanup → stale dirs accumulate.\n  82│- **Invariant:** every code path that creates an ephemeral perch cleans it before exit; exception: if the caller already had an active perch, do not treat as ephemeral and do not clean up.\n *83│- **spt-core mapping:** `ring` semantics carry; the daemon owns ephemeral-perch lifecycle, so a single guaranteed-cleanup (drop guard / RAII) is achievable in-process.\n  84│- **Sister cite:** `src/owl/ring.rs:58-294`.\n  85│\n  86│### 3.2 Stale signoff sentinel must not kill a fresh start\n    │...\n  89│- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\n *90│- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\n  91│\n  92│### 3.3 Orphan teardown fires echo-commune BEFORE INIT_SIGNOFF\n  93│- **Failure:** teardown delivers INIT_SIGNOFF without first saving the final context delta → Psyche signoff lacks the context-save summary.\n    │...\n 114│\n*115│### 4.3 Registry stale-entry cleanup precedes lookup\n 116│- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n 117│- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n 118│- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n    │...\n 273│- **Failure:** `atomic_write_bytes` staged every write under a **fixed** sibling `{name}.tmp`. Two processes writing the SAME target concurrently — traced at bind ~700µs apart: the daemon's `mutate_info` RMW and `spt api bind`'s `establish_perch` — both create the same `info.json.tmp`; whichever renames first CONSUMES it, and the loser's `fs::rename` hits `NotFound` (os error 2). `NotFound` is non-transient in `rename_with_retry`, so it surfaces as a hard write error: `establish_perch` returns `Bind...\n*274│- **Invariant:** concurrent atomic writers to the same target must never share a tmp name. Stage a **unique** tmp per write — `{name}.tmp.{pid}-{seq}`, `seq` from a process-local static `AtomicU64` (no clock, no rand → resume/replay-safe) — on BOTH the durable and non-durable paths (the collision is generic to `atomic_write`, not the fsync). Keep `rename_with_retry` + best-effort tmp cleanup on rename error. The new names still lack a `.json` extension, so exact-name / `*.json` loaders stay blind t...\n 275│- **spt-core mapping:** `spt_store::atomic::write_then_rename` (the shared core behind `atomic_write_bytes`/`_string` and the `_durable` siblings). Regression guard: a ≥4-thread `Barrier`-aligned hammer on one target (`concurrent_writers_never_collide_on_tmp`) — reds on the fixed-tmp code, greens on unique-tmp; a hammer that can't red is decorative. Cross-ref 5.13: the fsync widened the window, the fixed tmp name was the defect.\n 276│- **Source:** RCA 2026-07-01 (doyle gate rig) — write-trace side-channel + `cmd_bind` step probes pinned `PROBE cmd_bind establish ERR: Io(os error 2)`; explains the counter-39 gate-1 blanket failure, the dummy_harness ONLINE-race DIAGs, and the nondeterminism. Lesson: a widened timing window doesn't create a race, it reveals one.\n 277│\n    │...\n 294│\n*295│### 6.2 Soft-cleanup preserves state, removes `ready`\n*296│- **Failure:** hard-deleting a perch on cleanup loses spool (incl. stored signoff) needed for offline recovery.\n 297│- **Invariant:** soft-stop removes only the `ready`/online marker; preserves info + spool + dir. Hard-delete only on explicit operator action.\n 298│- **spt-core mapping:** instance offline-state recovery depends on this; carries to the daemon's stop path.\n 299│- **Sister cite:** `src/owl/stop.rs`.\n    │...\n 555│\n*556│<!-- [doc->REQ-HAZARD-CONTROL-STAMP-LIFETIME] -->\n*557│### 7.27 A control/viewer stamp must NEVER outlive its session — every teardown path clears what attach stamped  `[REQ-HAZARD-CONTROL-STAMP-LIFETIME]`\n 558│- **Failure (F-026 #2, live evidence HFENDULEAM):** an spt-hosted endpoint stayed `ONLINE+CONTROLLED` after the operator's RC `/exit` — hours later hall-a's `info.json` still read `controlled:true` (status offline, dormant) and hfenduleam still GOSSIPED `controller_node=self` cross-node (SPT_DEV.json snapshot). ROOT: `/exit` kills the CHILD, not the controller connection, so the endpoint suspends via child termination and the teardown runs the REAP path (`broker.rs` exit-waiter → `sessions.remove(&id...\n 559│- **Invariant:** on session reap the broker clears the perch's control/viewer stamps — `OutputLog::stamp_reaped()` = `set_driven_by(None)` + `set_controlled(false)` + `set_viewer_count(0)` via the known endpoint id — so no teardown path leaves a stamp behind. The broker stays the SINGLE writer; the clear is unconditional (idempotent when already clear) and RACE-FREE — the session is dead, so there is no live controller to re-stamp concurrently (the same argument as 7.15's no-session leg). Best-effo...\n 560│- **spt-core mapping:** impl `broker.rs` `OutputLog::stamp_reaped` + the exit-waiter reap call (before `sessions.remove`); int (real broker+PTY, no mocks) `crates/spt-daemon/tests/control_stamp_lifetime.rs` `reap_clears_control_and_viewer_stamps` (spawn hosted session → the live controller stamps `controlled=true` → latch `driven_by`+`viewer_count` on disk → KILL the child → the reap clears all three; RED-first verified: fix disabled leaves `driven_by=Some`, `controlled=true`, `viewer_count=Some(...\n    │...\n 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    │...\n 698│<!-- [doc->REQ-HAZARD-ENDPOINT-LIFECYCLE] -->\n*699│- **Failure (paid-for, three hertz reports + operator field 2026-07-16):** four families, one root shape — lifecycle state written by multiple non-converging paths, optimistic stamps never verified. (a) `cmd_listen` stamped `status=online` from manifest capability alone → dead-PID hybrid rows survived EVERY restart (reconcile skipped them by state, cleanup gated on `controllable=true`, readers trusted `status=online` past dead-PID rejection). (b) Definitive session death stamped `status=offline` only...\n*700│- **Invariant:** online is stamped only from persisted state + hosting authority; legacy rows self-heal after a SUCCESSFUL broker query (broker failure ≠ empty set); control-stamp cleanup runs for every endpoint absent from session truth regardless of state/controllability, split from offline classification; definitive death = ONE atomic store mutation (offline + rest_state=suspended + clear dormant_since_ms, same info.json write) — never a rest-event edge, never reader-side inference; stamp writes a...\n 701│- **spt-core mapping:** `spt/src/api/startup.rs` `cmd_listen` creator gate; `spt-daemon/src/livehost.rs` reconcile split + decide_resume; `spt-daemon/src/lifecycle.rs` `mark_offline` → terminal-normalize; `spt/src/cli.rs` `cmd_stop`/create dispatch; `spt-daemon/src/broker.rs` SpawnReq policy split + KIND_SPAWN_FRESH + dup-guard probe; KH 7.12/7.27/7.29 kin.\n 702│- **Source:** REGISTRY-LIFECYCLE W2/W3 (hertz emphasys triple + rest-normalize + spawn-fresh reports 2026-07-16, doyle seam-verified same day; ADR-0041; operator deployah-wedge field evidence).\n 703│\n    │...\n 714│| 2.4 | gen_start = now() on cold-start + handoff | per-instance generation |\n*715│| 3.1 | Ephemeral perch cleanup on all exit paths | `ring` (RAII guard) |\n 716│| 3.3 | Echo-commune before INIT_SIGNOFF | daemon psyche loop |\n 717│| 4.1 | Envelope decode order, `&amp;` last | spt-proto (public, wire-versioned) |\n 718│| 4.2 | Parser panic-free + tolerant | spt-proto |\n\n## adr/\n### 0041-endpoint-lifecycle-truth.md\n 17│   minting `ready_agent`/`controllable=false` hybrid rows born online-authoritative. The\n*18│   livehost reconcile skips `state != live_agent` rows AND gates cleanup on\n 19│   `controllable=true`, so a dead-PID hybrid survives every restart; `is_perch_alive`\n 20│   trusts `status=online` and skips dead-PID rejection — every reader faithfully\n 21│   resurrects the lie.\n   │...\n 45│   failure is never interpreted as an empty session set (no mass-offline on a hiccup).\n*46│2. **Control cleanup splits from offline classification.** Reconcile clears\n 47│   `controlled`/`driven_by`/`viewer_count` for EVERY endpoint absent from session truth —\n 48│   regardless of state or controllability — while offline classification keeps its narrow\n 49│   gate (live_agent + controllable). A row's classification quirk can never shield stale","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784365530533}}
{"type":"message","id":"5d69ab3b","parentId":"e7bf7e57","timestamp":"2026-07-18T09:05:36.960Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Examining picker for output differences**"},{"type":"toolCall","id":"call_OGjOsUPTawjpyVaYkGXmpFS7|fc_0ce5441d29b9f05d016a5b41dfa164819b877653bdf41c25b8","name":"irc","arguments":{"i":"Coordinating whitespace RCA","op":"send","to":"PickerWhitespaceRca","message":"Please send your picker whitespace root/seam and whether it shares any spt-term ScreenGrid/rc output code. I’m tracing broker PTY + rc cleanup.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":12997,"output":160,"cacheRead":166400,"cacheWrite":0,"totalTokens":179557,"reasoningTokens":68,"cost":{"input":0.064985,"output":0.0048000000000000004,"cacheRead":0.0832,"cacheWrite":0,"total":0.15298499999999998}},"stopReason":"toolUse","timestamp":1784365530581,"responseId":"resp_0ce5441d29b9f05d016a5b41dac504819ba869eefeafa0bf2f","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b41dd96c8819bbdae6d92f81060c7","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0HflR_ZI0t5SfCL10O8NlWJMlr4fQXBW_PLd2QxmdGGvxn-PsyA1TGisF5XBvC5nkYwb_3gWXv-D3omkmDoI_5T1IaVmlOp3UdImS9TwBisbv-rpzxfBP866rVr7wj_jml2NehBRNsmOzVbN7l1v-od67r1azERtIamPc2hXkvCUlkAR-8QDQ9_4zpL_MeGHSKR34iz1QLfr0ZwAW8FDyUBlgy4aL9VYXrI3IhC9WC0QxLNFQkkO0d-ykcpy1vOsVUWiIW8qvkmcOenOUTWt1wR6UlBZGpyeHdTf0EHoHclEdZT8o2LduTWoWueI18k6N5qrmIg18UmaXgAbQOgI5OLm0-9HPXYEAqbaQOI6X7NkB-d71Jarwn0TRy-W9jhBV3eBaQL_NDY-HL8xCYf8yjjMLSaxCOOEeKLR3pPrfIYVoS3Ss5HVhojo0i6lEA800qci7jsHo_kRThwPI5-GH6WvFLkVE2BxXUQTcbkqzvwji7O4LXcdzb4Ck72roe1hWi0CpkHwslfr4Z5pTkWAHu80cVNeU_X04grIlt-YXWf9Z8yStc-DLmgeWaNa9MV5KtNQuVYz2WiGrQu08Y-hwCjwdDm9UrQLOhpW9-RtYMBHvcUKQUtaHPvfH4deNAE-45Qf9_wHj9WDtKfXurXnuCMDZqognKGIYsBhIBzhztoBA1bSgWdmR9XeH2I_mu6faawTE2aAmhPOlTXP0uXmApAVBsPmEx_JdQzNQf-yzdXr8cjxkk5RrDvh7W0llEPvXB5vaV8aqN-bwljNJH8c07fOztl8oYSd1AAlbouS5C7zy0wbozSeV3oIXbUSZT3k8ynNYFJhl7letlXz5IiPWkLZmVN208iIRtBVEi8wmroNjo0U8ZSYvM0PYSTL59bunZxQfQMpWOcM50sMVI5RylF9BqLGnoDiR3x9pJoyL0otOnr79ZcRCM09ZbM2riON4_DHsRTDjAFWfHbFb59oy5QMz8rmWfRrZuAv6JPu2Ak6neRvZQZVPrGYQAi5_CafZx5I4uRdQ6mkRJlHcoVNNsQ6ismiImEH4wG03WTGGZhLgHFwQufcrTtrs4UkxsP7nzjtwbINZBTad9_5WnaLgJlkF1SaChlxErYBQBmiIwyWCJmEFJxGOo62yqUaqm474oGbio4lPyrdBpMiR84cnXQmBG0R0pNf2ViKHPk71anQbad5wD_tIv-9IlEgGteBFWQa5JAp1eUuyLrhpgXiFTzk25BrJFwA0NarKMJDdjJp3h8cSfd94IV5PskoQYl7DYbI4S6t9d-zpid6sew9IHitkTBol4RdweKrPfg53MFAIRCC4PuHhEXV6Ka2H-FlB-gyqnvyeDIE66Ba68xXqJm7QEp8IVlWEibWn83DS5mqlI=","summary":[{"type":"summary_text","text":"**Examining picker for output differences**"}]},{"id":"fc_0ce5441d29b9f05d016a5b41dfa164819b877653bdf41c25b8","type":"function_call","status":"completed","arguments":"{\"i\":\"Coordinating whitespace RCA\",\"op\":\"send\",\"to\":\"PickerWhitespaceRca\",\"message\":\"Please send your picker whitespace root/seam and whether it shares any spt-term ScreenGrid/rc output code. I’m tracing broker PTY + rc cleanup.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_OGjOsUPTawjpyVaYkGXmpFS7","name":"irc"}]},"duration":6378.414899975061,"ttft":3010.294599980116,"contextSnapshot":{"promptTokens":179397,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OGjOsUPTawjpyVaYkGXmpFS7|fc_0ce5441d29b9f05d016a5b41dfa164819b877653bdf41c25b8","toolName":"irc","startedAt":"2026-07-18T09:05:36.961Z","intent":"Coordinating whitespace RCA"},"id":"acd39a7b","parentId":"5d69ab3b","timestamp":"2026-07-18T09:05:36.961Z"}
{"type":"message","id":"2dc5eccc","parentId":"acd39a7b","timestamp":"2026-07-18T09:05:36.961Z","message":{"role":"toolResult","toolCallId":"call_OGjOsUPTawjpyVaYkGXmpFS7|fc_0ce5441d29b9f05d016a5b41dfa164819b877653bdf41c25b8","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- PickerWhitespaceRca: injected"}],"details":{"op":"send","from":"RcWhitespaceRca","to":"PickerWhitespaceRca","receipts":[{"to":"PickerWhitespaceRca","outcome":"injected"}]},"isError":false,"timestamp":1784365536961}}
{"type":"message","id":"21493f89","parentId":"2dc5eccc","timestamp":"2026-07-18T09:05:58.542Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Assessing ANSI clear sequence handling**\n\n**Identifying terminal exit cleanup race condition**\n\n**Investigating drain flush coordination on exit**"},{"type":"toolCall","id":"call_LGbMaL5AbZvGIbAED2fyschW|fc_0ce5441d29b9f05d016a5b41f661ac819ba7a917637c3ce48d","name":"grep","arguments":{"i":"Finding Drain drop behavior","pattern":"impl Drop for Drain|impl Drop for PtySession|drop\\(&mut self\\)|join\\(","path":"crates/spt-term/src/reader.rs; crates/spt-term/src/pty.rs; crates/spt-daemon/src/broker.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_22e6KM44B7qHL3ZLNufx9iB0|fc_0ce5441d29b9f05d016a5b41f661c0819bb6fcf9b6ee10414c","name":"read","arguments":{"i":"Reading broker output ordering tests","path":"crates/spt-daemon/src/broker.rs","selector":"6320-6425"}},{"type":"toolCall","id":"call_GknnMtOcwF4mrhhyWr7Ufjc9|fc_0ce5441d29b9f05d016a5b41f661c8819baf582d9ca0092a19","name":"grep","arguments":{"i":"Finding final output tests","pattern":"final output|tail|before exit|Exit.*Output|output.*Exit|drain.*exit|teardown|1049l|cursor.*show","path":"crates/spt-daemon/tests; crates/spt-daemon/src; crates/spt/tests; crates/spt/src; crates/spt-term/tests; crates/spt-term/src","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_vqk4FPHrB3ior7GiAAxzVHWy|fc_0ce5441d29b9f05d016a5b41f661d0819bbc200e5778ed9666","name":"read","arguments":{"i":"Reading terminal test fixtures","path":"crates/spt-daemon/tests/broker.rs","selector":"1-320"}},{"type":"toolCall","id":"call_WLlUgaq0iSqA7SU7MnP5Snza|fc_0ce5441d29b9f05d016a5b41f661d4819bbdb1a08a3d2ba316","name":"grep","arguments":{"i":"Finding session exit flow tests","pattern":"Exit|KIND_EXIT|kill_session|wait.*output|Output","path":"crates/spt-daemon/tests/broker.rs; crates/spt-daemon/tests/attach.rs; crates/spt/src/rc.rs","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":894,"output":865,"cacheRead":178688,"cacheWrite":0,"totalTokens":180447,"reasoningTokens":492,"cost":{"input":0.00447,"output":0.02595,"cacheRead":0.08934399999999999,"cacheWrite":0,"total":0.119764}},"stopReason":"toolUse","timestamp":1784365536963,"responseId":"resp_0ce5441d29b9f05d016a5b41e13454819baacbfcdcd802b3e8","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b41e33f00819ba68354ac3eebf8e5","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0H2wgFWHf7PfUaRScx409BT2ATlGphzjFiyPSUNqm5zW65oE5hjtQVmmKN5_MdyqUTPPhXb2Pgk4HhDbX7t44i7qELWjE6WP4NknhzEpwidU8QENkzQYCP43WLKroJ1BAvU5MUsBsnmB3MzHcxNYGtodn6GZrJqLxIWT58-87Yc3FxmaPspM4gd22NXBJ6xw1Syctwxqoj8lwGvw6IHs2WU1th7rBmE-Z_t9n6wL3BXbw2GT6ALQ0uXoidTfAKmDj4ACNY94vau0DT17_smYXMsISp4ePpv6SKe77wjzrX3JiAyEbPfsx5Z7QNf0zWM8l1FPFaVPa_IkkakOlH5hO6Y_x9KsmzWQWq3BJYyFcgsNpMeDTm71Ihdih_Zhv2QtcSMLnMHekGOgIyrFdZjCgNB03N0UGnV-kNyqkBD1PgHVW9FW8-qBG_U0F9m_QqewAcmCSlgX_3nW1umQJCcGLitdnVMSEiejxMXT2M5DlzFcVkBXD8uFB893RSFWY7eXZE704x4C9sb4OEadpfppiMac7XdKGOSCeQWvVdV4LRxrRbC5L0L2egiVN7En8PcMhgSpVVaYeIZrurgUrzK2dgxGQ2-HB85oAONKfXZBAUlZ1fxrPf5Lffk3FNmdgvdpamN7b10RUVvI01xVxkoWbxgB2HwmIQqNogpPN7DQUdI7Ht2j2qJOUhJeLiwYmfxiK3OYTUIoYpln4bpG-BfDChOyU06h4o1dm19N6hgxBg0Xjc9VILjRsTUYvzxLw54GdxiEyyi8iIPFpq7eJK7YwuHE5IZpXXQWALAEjnyFl8vV4pV8COJo9ywfQEm5PemQHw9HL3ZpzEWQmik6bVbpPdq-U4WJF9ReJ2f-mrpwVypRVRmMdge3VscB5gmtRifgpAcA6_9QRzIgRdQ1XlLLs9GL41OSHwsrb_E1gf420XOn_6l65qxaAlu__SZ1Qo0Oj83hQMnexfw_ZoayNalAL5810vaZ4C8SBhpZcl28yqxOXtUq9-YPHida9vk_njhr9Xgh96KIZky2LHq16vimmqfxAyJWh6kpY6gQL6VwqD5Q_D1J02cIw8ky_0EM0N0jImfluULQ2Iu-HGq9J_UurCiKet78Qnh1c416Fzup_8pLevPPxwGXRd_f7Mq-oYv22vJQp56gh2Trt-XtKC0zDbHyYOX_1SqMPhurx9NVwkSVxnUGFLbBPzRiakas9AsqMMeWD8N93VnQitBfT4oJSnE253mNjpAVz_wqnwPYDJr1FZvlsHZZDitpgkZEyxmtoahLRjaq8ag4U27mybzn9CMHpws_G8rYQj9UhyaVvq9UeorJ56iMn2fUmvEAsCFQ2NkVNIc5YVVNxRiieH6548odQJOxgUnTNbWjDRtuJWOsXOwwVbQtC9mt2tw4tJd6bCNYHuZ694qZ__5RyM2APyLF1MKGM-MqGNFJZIUmU9SORCruEWPdYwIRM64S9BgPErNbMvxu1mk_j-jzZRb_j1O9jtBWdxcsdLziWunswNrQM8HEB7sbxQ312uZw9IJudT8bkpQJ3TyCxS_WHA10tLkGgWWWTJ2lej9kF-ht6gyheXAC4Vggvkqw939Y2b7fjIUem28wCY7VNaOeoVraSGQ2nkQsydJxzWcHjKXMO6t_66rlUQ8WMqU9i8enO5SasvVnk3ZPUyCyy_kLseLvREEGnZSlQuhN31bgrYRgsCd3M2mwMJNukjcxlSQbBmQYtNf4maylc9nj5Nh43IsjPdPjLcVfKb79oCJa1mBaPkCAGCjyg9MOPuLv8tBG_Kj7jjV8N_Bv1CS-DzZJlbl-IWPIEoHUJ17l38OOIQZfBmxhzkt7U1SAJTfValci1z0xjst-FDRG7kV4qMrQLsF42U3fs44IZ4bJHuJ1OBWnXQebwASEEfmPkPjQptxmHzeywnwD0YuuE7bSJ2MvA-F7ucoHsMVNEcT04xnumvyoVEDRBpnXvK_1j04TggOUlZfEklaLj33lfA9sn1ZxpxmTbOG8dmORbmjBXY6MVaV4V2tAYHKR5hGgQ3HY7tOPVt4jr-wXHLYw8MpxFczyuvRDKI0sQLRRuHLSfE2-X6kmG29EMdB-SxM5fI844eHQYpy0enNRG1NEPQsfVOfYiu50XGPo79iyFSx8FS23qx2CLr9wfDbBWCJbJ_7Svlps2Z_Txf_Id9uCV_uAcJhxjxz_JvsiHA5Hc74VqJoqg--gGjMUsz6PMam5r_cGlUSG4bcKYbmXIJc39-1UDqV_eQZOEIL8X6QqpT4LNx2061azKNMomQ2jpedXVIcsT1H-XwxkSdwQGhth2R1ETwnwRGwqtSPW1ipKU5zljy9S0ufjDSZj7_kFUnU0xmW5Q0ttM24x3o0TuaJO5-18c2bYPXMVRqucQLphW-GAnwwTe90RMCBH_zBGLpuIDuGYZdD7ashyfGlolxLK_Km1GqVfrSWDOl6leqeBfEcCNmEWNiELg7PRfsLS2tn5Xo869QeMFEnVefTcd4d9PyncLIQe87LIWvdQGYeLJqPTYlgTRsMD5xisRhf_05q1C_hOWj3ARgJhaBjpUUXioVHa73xOTGfuX_zlKpoURc8pr5LtIR4W1rm_QfTI-JLAbTEhUXszT3TpUokXeHclv5SB4qbDazekT6jjbgiLnW_h-u6kyf5WkpQb8sBSW5HfmeCwT7cTXGJnKLdSVlVPeOZKxX-B40tcEiWwEFH3qyMYAuPHIfnDesl97eC6Cp7q267pfsD9soOcVFLsyWBGsdhnlzuWC_2DHqpeWIZHTxxOgKCs7VQ55xnoEJU-uoOePbKj_aseuehPiKRCXq7jarl-Z7MtUPn-_P0KPXxFL8GU8ypb-AsTCVnV_4-xLFRz39EQVjeYNDJMSTWl6JKtri6iKNaATGpPoNmYDwZwlKsJ6kXGbwN-FAEcel02SjgYcqvHzJViV44Oj34MbGPhiEgdt6GNb7V0HXEMhImFA3iX17WZmPKleHYBVq3cjxiXB7l5tKM7eVcJJ0f9NIGJag6biqcxDRwgN-T44dUG2ifbjTcH8DuaF7IsvSF5tYNYFMIGoxghYLirbyLcXRvud-8u5tbLwdKt-J08Q1UeyIFUn_dqACxj1LZC-8V-IZ1oXhZ0vqDYzbc3UGbLhKE1NvWHjcZXym_qqlnMQA0odL4P14_V7FxdPnKI_qevPmEAn-9RQkcP0XY1Exx6_1miVOGrStonD5p-2uE9El9yn_s2-_sNab1KOBcRfpJnKzq2p3WmdiPazKbzXvuEUW3d7uYD46-J4aW3ySx3wwZSdcdnCBlBltX_uXjtOMrDAtrPHuM7GILMlx-LNmhbTL_VuQ0Ys8y9se1BI5gQGd7uxlU2DPTFFntuIr_CXvWt82tjP9J34G1au6_xVHdyk1zkzlqpssQRfJGmvXqcCgrND5MrjSg6WHviVS1weAP0Ju9TSnQGsL0gc7vCvMyjq9K2MTMK3ydHHBG9nAh5jBFIshgL2p6Krkya0ydeeadClE6_EnL-G3EDDTeFVs_560qU9TDASmZP-zk-69fYIAtop1kR5W_TgH-XhxtSrw7S0tOq_jCy2A-OZI-ayhIaIRIADvztWOrSFUS-JpQcY2kVJvcwHmgITIDrI2TN7HNmgwkgxUfIQzbcVEYHZ5Imay0w1wdXA6CIARgz5iGLeohDlcNegJpVzdw2E9s1MqjBbF1U14l7_ZaK1wRrogbJrNNwNLJW73A34DVgAlj3xnClM9zPIFNnOKktej-HvchD5iBaz6ynFYwkXzCkxU_KVy_nmj2JLysYoU2yRxbBQV8t4Lr-7O0eF-qssy-v7FYOtS7912euvBHkRlAV4ZRSzcJFugPB1Hub3RQeduTd9jaSDGES8B6flfGgKg3PbsFoeX8nMqGxlKBJ1myoTNEfnUA2LqYXq3Aij-ArWI89m4SuQ9XBX2nwdC5idryhoM5bqO8i7OOUXGgkwe55rtS70HQX3IPi6oLsrODM114zA==","summary":[{"type":"summary_text","text":"**Assessing ANSI clear sequence handling**"},{"type":"summary_text","text":"**Identifying terminal exit cleanup race condition**"},{"type":"summary_text","text":"**Investigating drain flush coordination on exit**"}]},{"id":"fc_0ce5441d29b9f05d016a5b41f661ac819ba7a917637c3ce48d","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding Drain drop behavior\",\"pattern\":\"impl Drop for Drain|impl Drop for PtySession|drop\\\\(&mut self\\\\)|join\\\\(\",\"path\":\"crates/spt-term/src/reader.rs; crates/spt-term/src/pty.rs; crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_LGbMaL5AbZvGIbAED2fyschW","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41f661c0819bb6fcf9b6ee10414c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading broker output ordering tests\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"6320-6425\"}","call_id":"call_22e6KM44B7qHL3ZLNufx9iB0","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41f661c8819baf582d9ca0092a19","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding final output tests\",\"pattern\":\"final output|tail|before exit|Exit.*Output|output.*Exit|drain.*exit|teardown|1049l|cursor.*show\",\"path\":\"crates/spt-daemon/tests; crates/spt-daemon/src; crates/spt/tests; crates/spt/src; crates/spt-term/tests; crates/spt-term/src\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_GknnMtOcwF4mrhhyWr7Ufjc9","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b41f661d0819bbc200e5778ed9666","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading terminal test fixtures\",\"path\":\"crates/spt-daemon/tests/broker.rs\",\"selector\":\"1-320\"}","call_id":"call_vqk4FPHrB3ior7GiAAxzVHWy","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41f661d4819bbdb1a08a3d2ba316","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding session exit flow tests\",\"pattern\":\"Exit|KIND_EXIT|kill_session|wait.*output|Output\",\"path\":\"crates/spt-daemon/tests/broker.rs; crates/spt-daemon/tests/attach.rs; crates/spt/src/rc.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_WLlUgaq0iSqA7SU7MnP5Snza","name":"grep"}]},"duration":21577.365700006485,"ttft":2279.8995999991894,"contextSnapshot":{"promptTokens":179582,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_LGbMaL5AbZvGIbAED2fyschW|fc_0ce5441d29b9f05d016a5b41f661ac819ba7a917637c3ce48d","toolName":"grep","startedAt":"2026-07-18T09:05:58.542Z","args":{"path":"crates/spt-term/src/reader.rs; crates/spt-term/src/pty.rs; crates/spt-daemon/src/broker.rs"},"intent":"Finding Drain drop behavior"},"id":"4cd6528a","parentId":"21493f89","timestamp":"2026-07-18T09:05:58.542Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_22e6KM44B7qHL3ZLNufx9iB0|fc_0ce5441d29b9f05d016a5b41f661c0819bb6fcf9b6ee10414c","toolName":"read","startedAt":"2026-07-18T09:05:58.542Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading broker output ordering tests"},"id":"3ad73e2e","parentId":"4cd6528a","timestamp":"2026-07-18T09:05:58.542Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_GknnMtOcwF4mrhhyWr7Ufjc9|fc_0ce5441d29b9f05d016a5b41f661c8819baf582d9ca0092a19","toolName":"grep","startedAt":"2026-07-18T09:05:58.542Z","args":{"path":"crates/spt-daemon/tests; crates/spt-daemon/src; crates/spt/tests; crates/spt/src; crates/spt-term/tests; crates/spt-term/src"},"intent":"Finding final output tests"},"id":"f594b0bc","parentId":"3ad73e2e","timestamp":"2026-07-18T09:05:58.542Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vqk4FPHrB3ior7GiAAxzVHWy|fc_0ce5441d29b9f05d016a5b41f661d0819bbc200e5778ed9666","toolName":"read","startedAt":"2026-07-18T09:05:58.542Z","args":{"path":"crates/spt-daemon/tests/broker.rs"},"intent":"Reading terminal test fixtures"},"id":"2b74caee","parentId":"f594b0bc","timestamp":"2026-07-18T09:05:58.542Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_WLlUgaq0iSqA7SU7MnP5Snza|fc_0ce5441d29b9f05d016a5b41f661d4819bbdb1a08a3d2ba316","toolName":"grep","startedAt":"2026-07-18T09:05:58.542Z","args":{"path":"crates/spt-daemon/tests/broker.rs; crates/spt-daemon/tests/attach.rs; crates/spt/src/rc.rs"},"intent":"Finding session exit flow tests"},"id":"e689652b","parentId":"2b74caee","timestamp":"2026-07-18T09:05:58.542Z"}
{"type":"message","id":"7a617a72","parentId":"e689652b","timestamp":"2026-07-18T09:05:58.549Z","message":{"role":"toolResult","toolCallId":"call_LGbMaL5AbZvGIbAED2fyschW|fc_0ce5441d29b9f05d016a5b41f661ac819ba7a917637c3ce48d","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-term/src/\n### reader.rs\n 95|    /// on ConPTY that means the child must be torn down first. Best-effort.\n*96|    pub fn join(mut self) {\n 97|        if let Some(h) = self.handle.take() {\n*98|            let _ = h.join();\n 99|        }\n 100|    }\n 101|}\n...\n 250|        });\n*251|        drain.join(); // ScriptedReader EOFs, so this returns\n 252|\n 253|        assert_eq!(\n 254|            &*written.lock().unwrap(),\n...\n 281|        let answered = drain.dsr_answered();\n*282|        drain.join();\n 283|        assert_eq!(answered, 0, \"no DSR query → no CPR answer\");\n 284|        assert!(written.lock().unwrap().is_empty(), \"nothing written back\");\n 285|    }\n\n## spt-daemon/src/\n### broker.rs\n 212|impl Drop for WakeClaimGuard<'_> {\n*213|    fn drop(&mut self) {\n 214|        recover(self.set).remove(&self.endpoint);\n 215|    }\n 216|}\n...\n 1553|    impl Drop for ExitNote {\n*1554|        fn drop(&mut self) {\n 1555|            self.send.lifecycle_event(\n 1556|                \"writer-exit\",\n 1557|                &format!(\"role=viewer session={} reason={}\", self.session_id, self.reason),\n...\n 2151|fn read_translation_command(install_dir: &std::path::Path) -> Option<Vec<String>> {\n*2152|    let s = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).ok()?;\n 2153|    let manifest = spt_runtime::Manifest::from_toml_str(&s).ok()?;\n 2154|    let decl = manifest.message_idle_translation_binary.as_ref()?;\n 2155|    // F-019 / REQ-INSTALL-11 + ADR-0029: resolve `command` (preferred) else the\n...\n 3157|        impl Drop for HandlerTally<'_> {\n*3158|            fn drop(&mut self) {\n 3159|                self.0.fetch_sub(1, Ordering::AcqRel);\n 3160|            }\n 3161|        }\n...\n 3268|                KIND_PAIR_JOIN => {\n*3269|                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\n 3270|                        send_error(&send, &msg);\n 3271|                    }\n 3272|                }\n...\n 4447|    // [impl->REQ-SUBNET-2]\n*4448|    fn dispatch_pair_join(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4449|        let req: PairJoinReq = serde_json::from_value(env.payload)\n 4450|            .map_err(|e| format!(\"bad pair-join payload: {e}\"))?;\n 4451|        let host = self\n...\n 4463|        };\n*4464|        let frame = match host.pair_join(&req.subnet, &req.code, responder_addr) {\n 4465|            Ok(outcome) => Envelope::new(\n 4466|                KIND_PAIR_JOINED,\n 4467|                serde_json::to_value(PairJoinReply {\n...\n 4581|            .ok_or(\"net disabled: this broker was bound without a network host\")?;\n*4582|        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {\n 4583|            Ok(outcome) => {\n 4584|                recover(&self.pair_holds).remove(&req.session_id);\n 4585|                Envelope::new(\n...\n 4977|pub(crate) fn default_journal_path() -> std::path::PathBuf {\n*4978|    spt_store::perch::spt_home().join(\"daemon-effects.log\")\n 4979|}\n 4980|\n 4981|/// Write a frame to the brain, ignoring failures (brain gone / conn poisoned).\n...\n 5260|            drop(tx);\n*5261|            writer.join().expect(\"evicted writer joins\");\n 5262|\n 5263|            let mut client = client;\n 5264|            let env = read_frame(&mut client).expect(\"an evicted close writes a final frame\");\n...\n 5285|            drop(tx);\n*5286|            writer.join().expect(\"normal writer joins\");\n 5287|            // Drop the broker's send-half clone so the client read sees EOF (otherwise\n 5288|            // the half stays open and the blocking read would hang).\n 5289|            drop(send);\n...\n 5665|        })\n*5666|        .join();\n 5667|        assert!(floor.is_poisoned(), \"precondition: the floor mutex is poisoned\");\n 5668|        // The fix: the recovered guard is fully usable — open() takes, is_held reads.\n 5669|        lock_floor(&floor).open();\n...\n 5692|        })\n*5693|        .join();\n 5694|        assert!(sessions.is_poisoned(), \"precondition: the sessions mutex is poisoned\");\n 5695|        // The fix: the recovered guard is fully usable — the prior row survives and a\n 5696|        // NEW attach can still insert/look up (no permanent wedge).\n...\n 5750|        })\n*5751|        .join();\n 5752|        assert!(log.is_poisoned(), \"precondition: the log mutex is poisoned\");\n 5753|        let g = recover_log(&log);\n 5754|        assert!(g.ring.is_empty(), \"recover_log clamps the torn ring to empty on recovery\");\n...\n 5979|        let dir = tempfile::tempdir().unwrap();\n*5980|        let journal = EffectJournal::open(dir.path().join(\"effects.log\")).unwrap();\n 5981|        let (w, rx) = test_input_writer(16);\n 5982|        let key = EffectKey {\n 5983|            class: 1,\n...\n 6717|                        [message-idle-translation-binary]\\npath = \\\"some/bin\\\"\\n\";\n*6718|        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 6719|        assert_eq!(\n 6720|            read_translation_command(dir.path()),\n 6721|            Some(vec![\"some/bin\".to_string()]),\n...\n 6731|                         command = \\\"{adapter_dir}/claude-spt translate\\\"\\n\";\n*6732|        std::fs::write(dir2.path().join(\"manifest.toml\"), manifest2).unwrap();\n 6733|        let argv = read_translation_command(dir2.path()).expect(\"command resolves\");\n 6734|        assert_eq!(argv.len(), 2, \"program + one arg: {argv:?}\");\n 6735|        assert!(\n...\n 6754|                        min_spt_core_version=\\\"1\\\"\\n\";\n*6755|        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 6756|        assert_eq!(\n 6757|            read_translation_command(dir.path()),\n 6758|            None,"}],"details":{"scopePath":"crates/spt-term/src/reader.rs, crates/spt-term/src/pty.rs, crates/spt-daemon/src/broker.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":22,"fileCount":2,"files":["crates/spt-term/src/reader.rs","crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-term/src/reader.rs","count":4},{"path":"crates/spt-daemon/src/broker.rs","count":18}],"truncated":false,"displayContent":"# crates/\n\n## spt-term/src/\n### reader.rs\n  95│    /// on ConPTY that means the child must be torn down first. Best-effort.\n *96│    pub fn join(mut self) {\n  97│        if let Some(h) = self.handle.take() {\n *98│            let _ = h.join();\n  99│        }\n 100│    }\n 101│}\n    │...\n 250│        });\n*251│        drain.join(); // ScriptedReader EOFs, so this returns\n 252│\n 253│        assert_eq!(\n 254│            &*written.lock().unwrap(),\n    │...\n 281│        let answered = drain.dsr_answered();\n*282│        drain.join();\n 283│        assert_eq!(answered, 0, \"no DSR query → no CPR answer\");\n 284│        assert!(written.lock().unwrap().is_empty(), \"nothing written back\");\n 285│    }\n\n## spt-daemon/src/\n### broker.rs\n  212│impl Drop for WakeClaimGuard<'_> {\n *213│    fn drop(&mut self) {\n  214│        recover(self.set).remove(&self.endpoint);\n  215│    }\n  216│}\n     │...\n 1553│    impl Drop for ExitNote {\n*1554│        fn drop(&mut self) {\n 1555│            self.send.lifecycle_event(\n 1556│                \"writer-exit\",\n 1557│                &format!(\"role=viewer session={} reason={}\", self.session_id, self.reason),\n     │...\n 2151│fn read_translation_command(install_dir: &std::path::Path) -> Option<Vec<String>> {\n*2152│    let s = std::fs::read_to_string(install_dir.join(\"manifest.toml\")).ok()?;\n 2153│    let manifest = spt_runtime::Manifest::from_toml_str(&s).ok()?;\n 2154│    let decl = manifest.message_idle_translation_binary.as_ref()?;\n 2155│    // F-019 / REQ-INSTALL-11 + ADR-0029: resolve `command` (preferred) else the\n     │...\n 3157│        impl Drop for HandlerTally<'_> {\n*3158│            fn drop(&mut self) {\n 3159│                self.0.fetch_sub(1, Ordering::AcqRel);\n 3160│            }\n 3161│        }\n     │...\n 3268│                KIND_PAIR_JOIN => {\n*3269│                    if let Err(msg) = self.dispatch_pair_join(env, &send) {\n 3270│                        send_error(&send, &msg);\n 3271│                    }\n 3272│                }\n     │...\n 4447│    // [impl->REQ-SUBNET-2]\n*4448│    fn dispatch_pair_join(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {\n 4449│        let req: PairJoinReq = serde_json::from_value(env.payload)\n 4450│            .map_err(|e| format!(\"bad pair-join payload: {e}\"))?;\n 4451│        let host = self\n     │...\n 4463│        };\n*4464│        let frame = match host.pair_join(&req.subnet, &req.code, responder_addr) {\n 4465│            Ok(outcome) => Envelope::new(\n 4466│                KIND_PAIR_JOINED,\n 4467│                serde_json::to_value(PairJoinReply {\n     │...\n 4581│            .ok_or(\"net disabled: this broker was bound without a network host\")?;\n*4582│        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {\n 4583│            Ok(outcome) => {\n 4584│                recover(&self.pair_holds).remove(&req.session_id);\n 4585│                Envelope::new(\n     │...\n 4977│pub(crate) fn default_journal_path() -> std::path::PathBuf {\n*4978│    spt_store::perch::spt_home().join(\"daemon-effects.log\")\n 4979│}\n 4980│\n 4981│/// Write a frame to the brain, ignoring failures (brain gone / conn poisoned).\n     │...\n 5260│            drop(tx);\n*5261│            writer.join().expect(\"evicted writer joins\");\n 5262│\n 5263│            let mut client = client;\n 5264│            let env = read_frame(&mut client).expect(\"an evicted close writes a final frame\");\n     │...\n 5285│            drop(tx);\n*5286│            writer.join().expect(\"normal writer joins\");\n 5287│            // Drop the broker's send-half clone so the client read sees EOF (otherwise\n 5288│            // the half stays open and the blocking read would hang).\n 5289│            drop(send);\n     │...\n 5665│        })\n*5666│        .join();\n 5667│        assert!(floor.is_poisoned(), \"precondition: the floor mutex is poisoned\");\n 5668│        // The fix: the recovered guard is fully usable — open() takes, is_held reads.\n 5669│        lock_floor(&floor).open();\n     │...\n 5692│        })\n*5693│        .join();\n 5694│        assert!(sessions.is_poisoned(), \"precondition: the sessions mutex is poisoned\");\n 5695│        // The fix: the recovered guard is fully usable — the prior row survives and a\n 5696│        // NEW attach can still insert/look up (no permanent wedge).\n     │...\n 5750│        })\n*5751│        .join();\n 5752│        assert!(log.is_poisoned(), \"precondition: the log mutex is poisoned\");\n 5753│        let g = recover_log(&log);\n 5754│        assert!(g.ring.is_empty(), \"recover_log clamps the torn ring to empty on recovery\");\n     │...\n 5979│        let dir = tempfile::tempdir().unwrap();\n*5980│        let journal = EffectJournal::open(dir.path().join(\"effects.log\")).unwrap();\n 5981│        let (w, rx) = test_input_writer(16);\n 5982│        let key = EffectKey {\n 5983│            class: 1,\n     │...\n 6717│                        [message-idle-translation-binary]\\npath = \\\"some/bin\\\"\\n\";\n*6718│        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 6719│        assert_eq!(\n 6720│            read_translation_command(dir.path()),\n 6721│            Some(vec![\"some/bin\".to_string()]),\n     │...\n 6731│                         command = \\\"{adapter_dir}/claude-spt translate\\\"\\n\";\n*6732│        std::fs::write(dir2.path().join(\"manifest.toml\"), manifest2).unwrap();\n 6733│        let argv = read_translation_command(dir2.path()).expect(\"command resolves\");\n 6734│        assert_eq!(argv.len(), 2, \"program + one arg: {argv:?}\");\n 6735│        assert!(\n     │...\n 6754│                        min_spt_core_version=\\\"1\\\"\\n\";\n*6755│        std::fs::write(dir.path().join(\"manifest.toml\"), manifest).unwrap();\n 6756│        assert_eq!(\n 6757│            read_translation_command(dir.path()),\n 6758│            None,"},"isError":false,"timestamp":1784365558549}}
{"type":"message","id":"07963c7c","parentId":"7a617a72","timestamp":"2026-07-18T09:05:58.558Z","message":{"role":"toolResult","toolCallId":"call_vqk4FPHrB3ior7GiAAxzVHWy|fc_0ce5441d29b9f05d016a5b41f661d0819bbc200e5778ed9666","toolName":"read","content":[{"type":"text","text":"//! B1 integration: a [`Broker`] hosts one real PTY child and relays its output\n//! and injected input over the versioned local IPC, on both OSes.\n//!\n//! The child is an echo program (`cat` on Unix, `findstr \".\"` on Windows — the\n//! same cross-OS shape spt-term's injection tests use): bytes the brain injects\n//! round-trip through the real PTY and come back as `output` events. We then\n//! kill it and observe the `exit` event. This exercises the whole B1 contract —\n//! spawn under a broker PTY, stream output, accept input, report exit — through\n//! the actual named-pipe / UDS transport.\n//!\n//! The brain is driven **single-threaded**: send a command, then read frames\n//! until the expected reply/event arrives (accumulating any interleaved output).\n//! interprocess `Stream::split()` shares the underlying fd, so a split client\n//! would only signal EOF once *both* halves drop; keeping one `Stream` means\n//! dropping it closes the fd, the broker's command loop hits EOF, and it tears\n//! the session down deterministically — which we then assert.\n// [int->REQ-DAEMON-2]\n\nuse std::sync::atomic::{AtomicU32, Ordering};\nuse std::sync::Arc;\nuse std::thread;\nuse std::time::Duration;\n\nuse interprocess::local_socket::Stream;\nuse spt_daemon::codec::{read_frame, write_frame};\nuse spt_daemon::effect::{EffectKey, Minter};\nuse spt_daemon::frame::{Envelope, Role};\nuse spt_daemon::msg::{\n    decode_bytes, encode_bytes, AppliedEvent, EndpointInjected, EndpointInputReq, ExitEvent,\n    InputReq, KillReq, OutputEvent, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED,\n    KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,\n    KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\n};\nuse spt_daemon::transport::{send_hello, LocalSocketTransport};\nuse spt_daemon::Broker;\n\nstatic SEQ: AtomicU32 = AtomicU32::new(0);\nfn unique_name() -> String {\n    let n = SEQ.fetch_add(1, Ordering::Relaxed);\n    format!(\"spt-daemon-b1-{}-{}.sock\", std::process::id(), n)\n}\n\n/// The echo child: reads stdin, writes each line to stdout.\nfn echo_spawn_req() -> SpawnReq {\n    #[cfg(unix)]\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\n    #[cfg(windows)]\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\n    SpawnReq {\n        program,\n        args,\n        rows: 24,\n        cols: 80,\n        endpoint: String::new(),\n        cwd: None,\n        env: Default::default(),\n        translation_binary: None,\n        adapter: String::new(),\n        install_dir: None,\n    }\n}\n\nfn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {\n    write_frame(conn, &Envelope::new(kind, payload)).expect(\"send frame\");\n}\n\nfn main_test() {\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n\n    // The broker accepts one brain and serves it on a handler thread.\n    let server_broker = Arc::clone(&broker);\n    let server = thread::spawn(move || {\n        let handle = server_broker.serve_once().expect(\"accept brain\");\n        handle.join().expect(\"handler thread\");\n    });\n\n    // Brain side: connect (retry briefly in case accept lags the bind).\n    let mut conn = None;\n    for _ in 0..200 {\n        match LocalSocketTransport::connect(&name) {\n            Ok(c) => {\n                conn = Some(c);\n                break;\n            }\n            Err(_) => thread::sleep(Duration::from_millis(10)),\n        }\n    }\n    let mut conn = conn.expect(\"connect to broker\");\n    // (No per-read timeout: Windows named pipes reject I/O timeouts. The broker\n    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads\n    // below return; a genuine stall is caught by the test runner's own timeout.)\n\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n\n    // Spawn the echo child under a broker PTY; read until the `spawned` reply,\n    // accumulating any output that interleaves.\n    send(\n        &mut conn,\n        KIND_SPAWN,\n        serde_json::to_value(echo_spawn_req()).unwrap(),\n    );\n    let mut out = Vec::<u8>::new();\n    let sid = loop {\n        let f = read_frame(&mut conn).expect(\"frame before spawned\");\n        match f.kind.as_str() {\n            KIND_SPAWNED => {\n                break serde_json::from_value::<Spawned>(f.payload)\n                    .unwrap()\n                    .session_id;\n            }\n            KIND_OUTPUT => accumulate_output(&f, &mut out),\n            _ => {}\n        }\n    };\n\n    // Inject a line; it must round-trip through the real PTY back as output.\n    send(\n        &mut conn,\n        KIND_INPUT,\n        serde_json::to_value(InputReq {\n            session_id: sid,\n            data_b64: encode_bytes(b\"INJECT-ALPHA\\r\"),\n            op_id: None,\n            minter: None,\n            ack: true,\n        })\n        .unwrap(),\n    );\n    let echoed = loop {\n        if String::from_utf8_lossy(&out).contains(\"INJECT-ALPHA\") {\n            break true;\n        }\n        match read_frame(&mut conn) {\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\n            Ok(_) => {}\n            Err(_) => break false, // recv timeout → echo never arrived\n        }\n    };\n\n    // Kill the child and read until the `exit` event.\n    send(\n        &mut conn,\n        KIND_KILL,\n        serde_json::to_value(KillReq { session_id: sid }).unwrap(),\n    );\n    let mut exit: Option<ExitEvent> = None;\n    loop {\n        match read_frame(&mut conn) {\n            Ok(f) if f.kind == KIND_EXIT => {\n                exit = Some(serde_json::from_value(f.payload).unwrap());\n                break;\n            }\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\n            Ok(_) => {}\n            Err(_) => break, // timeout\n        }\n    }\n\n    // Disconnect: dropping the single Stream closes the fd, so the broker's\n    // command loop hits EOF and tears the session down. Joining the handler\n    // (via the server thread) lets us assert the teardown happened.\n    drop(conn);\n    server.join().expect(\"server thread\");\n\n    assert!(\n        echoed,\n        \"injected input must round-trip through the PTY: {:?}\",\n        String::from_utf8_lossy(&out)\n    );\n    let exit = exit.expect(\"exit event after kill\");\n    assert_eq!(exit.session_id, sid);\n    assert_eq!(broker.session_count(), 0, \"session torn down on disconnect\");\n}\n\nfn accumulate_output(frame: &Envelope, out: &mut Vec<u8>) {\n    let ev: OutputEvent = serde_json::from_value(frame.payload.clone()).unwrap();\n    out.extend_from_slice(&decode_bytes(&ev.data_b64).unwrap());\n}\n\n#[test]\nfn broker_hosts_pty_child_streams_output_and_accepts_input() {\n    main_test();\n}\n\n/// REQ-SEND-SPT-HOSTED / REQ-MSG-IDLE-TRANSLATION-BINARY (ADR-0022 amendment): an\n/// inbound message addressed by ENDPOINT ID (`KIND_ENDPOINT_INPUT`) is delivered to\n/// an spt-hosted session ONLY through its translation binary. With NO translation\n/// binary declared (this echo child), idle delivery has no working binary → the\n/// broker acks `delivered=false` and writes NOTHING to the PTY (the caller spools;\n/// raw `payload+\\r` was removed as a delivery path — it did not submit on a modern\n/// TUI, the silent pseudo-delivery that masked F-019). An inject for an UNKNOWN\n/// endpoint likewise replies `delivered=false` and writes nothing. (The happy-path\n/// delivery — a real binary's choreography reaching the PTY — is the\n/// inject_control_wedge.rs int gate, which spawns the xlate_choreo_fixture binary.)\n// [int->REQ-SEND-SPT-HOSTED]\n// [int->REQ-MSG-IDLE-TRANSLATION-BINARY]\n// [int->REQ-HAZARD-IDLE-SILENT-NONDELIVERY]\nfn endpoint_input_test() {\n    let name = unique_name();\n    let broker = Broker::bind(&name).expect(\"bind broker\");\n    let server_broker = Arc::clone(&broker);\n    let server = thread::spawn(move || {\n        let handle = server_broker.serve_once().expect(\"accept brain\");\n        handle.join().expect(\"handler thread\");\n    });\n\n    let mut conn = None;\n    for _ in 0..200 {\n        match LocalSocketTransport::connect(&name) {\n            Ok(c) => {\n                conn = Some(c);\n                break;\n            }\n            Err(_) => thread::sleep(Duration::from_millis(10)),\n        }\n    }\n    let mut conn = conn.expect(\"connect to broker\");\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n\n    // Spawn the echo child LABELLED with an endpoint id (the spawn.endpoint the\n    // broker tracks per session — what KIND_ENDPOINT_INPUT resolves against).\n    let mut spawn = echo_spawn_req();\n    spawn.endpoint = \"wall-b\".to_string();\n    send(&mut conn, KIND_SPAWN, serde_json::to_value(spawn).unwrap());\n    let mut out = Vec::<u8>::new();\n    loop {\n        let f = read_frame(&mut conn).expect(\"frame before spawned\");\n        match f.kind.as_str() {\n            KIND_SPAWNED => break,\n            KIND_OUTPUT => accumulate_output(&f, &mut out),\n            _ => {}\n        }\n    }\n\n    // Endpoint-keyed inject (NO session_id — resolved by endpoint) into a hosted\n    // session that declares NO translation binary: idle delivery is\n    // translation-binary-ONLY, so there is no working binary → the broker acks\n    // delivered=FALSE and writes NOTHING to the PTY (the caller spools).\n    send(\n        &mut conn,\n        KIND_ENDPOINT_INPUT,\n        serde_json::to_value(EndpointInputReq {\n            endpoint: \"wall-b\".to_string(),\n            data_b64: encode_bytes(b\"ENDPOINT-INJECT-BETA\\r\"),\n            native: false,\n        })\n        .unwrap(),\n    );\n    let mut delivered: Option<bool> = None;\n    while delivered.is_none() {\n        match read_frame(&mut conn) {\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\n            Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {\n                delivered = Some(\n                    serde_json::from_value::<EndpointInjected>(f.payload)\n                        .unwrap()\n                        .delivered,\n                );\n            }\n            Ok(_) => {}\n            Err(_) => break,\n        }\n    }\n    assert_eq!(\n        delivered,\n        Some(false),\n        \"a hosted session with no translation binary acks delivered=false (spool, not raw inject)\"\n    );\n    assert!(\n        !String::from_utf8_lossy(&out).contains(\"ENDPOINT-INJECT-BETA\"),\n        \"no working translation binary → nothing is written to the PTY: {:?}\",\n        String::from_utf8_lossy(&out)\n    );\n\n    // Inject for an UNKNOWN endpoint: delivered=false, nothing written.\n    send(\n        &mut conn,\n        KIND_ENDPOINT_INPUT,\n        serde_json::to_value(EndpointInputReq {\n            endpoint: \"no-such-endpoint\".to_string(),\n            data_b64: encode_bytes(b\"SHOULD-NOT-APPEAR\\r\"),\n            native: false,\n        })\n        .unwrap(),\n    );\n    let mut miss: Option<bool> = None;\n    while miss.is_none() {\n        match read_frame(&mut conn) {\n            Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {\n                miss = Some(\n                    serde_json::from_value::<EndpointInjected>(f.payload)\n                        .unwrap()\n                        .delivered,\n                );\n            }\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\n            Ok(_) => {}\n            Err(_) => break,\n        }\n    }\n    assert_eq!(miss, Some(false), \"unknown endpoint acks delivered=false\");\n    assert!(\n        !String::from_utf8_lossy(&out).contains(\"SHOULD-NOT-APPEAR\"),\n        \"a miss writes nothing to any PTY\"\n    );\n\n    drop(conn);\n    server.join().expect(\"server thread\");\n}\n\n#[test]\nfn endpoint_keyed_inject_without_binary_spools_not_pty() {\n    endpoint_input_test();\n}\n\n/// REQ-HAZARD-ENV-SUBST: a SpawnReq's `env` map reaches the spawned PTY child's\n/// environment — the broker leg of the F-013 fix (a flagless harness gets\n/// SPT_ENDPOINT_ID populated, instead of an empty value → seed-by-PPID → zero\n/// perch). Spawn a child that ECHOES the injected env var; its output must carry\n/// the value.\n// [int->REQ-HAZARD-ENV-SUBST]\nfn env_inject_test() {\n…\n}\n\n[Showing lines 1-323 of 1221. Use :324 to continue]"}],"details":{"truncation":{"content":"//! B1 integration: a [`Broker`] hosts one real PTY child and relays its output\r\n//! and injected input over the versioned local IPC, on both OSes.\r\n//!\r\n//! The child is an echo program (`cat` on Unix, `findstr \".\"` on Windows — the\r\n//! same cross-OS shape spt-term's injection tests use): bytes the brain injects\r\n//! round-trip through the real PTY and come back as `output` events. We then\r\n//! kill it and observe the `exit` event. This exercises the whole B1 contract —\r\n//! spawn under a broker PTY, stream output, accept input, report exit — through\r\n//! the actual named-pipe / UDS transport.\r\n//!\r\n//! The brain is driven **single-threaded**: send a command, then read frames\r\n//! until the expected reply/event arrives (accumulating any interleaved output).\r\n//! interprocess `Stream::split()` shares the underlying fd, so a split client\r\n//! would only signal EOF once *both* halves drop; keeping one `Stream` means\r\n//! dropping it closes the fd, the broker's command loop hits EOF, and it tears\r\n//! the session down deterministically — which we then assert.\r\n// [int->REQ-DAEMON-2]\r\n\r\nuse std::sync::atomic::{AtomicU32, Ordering};\r\nuse std::sync::Arc;\r\nuse std::thread;\r\nuse std::time::Duration;\r\n\r\nuse interprocess::local_socket::Stream;\r\nuse spt_daemon::codec::{read_frame, write_frame};\r\nuse spt_daemon::effect::{EffectKey, Minter};\r\nuse spt_daemon::frame::{Envelope, Role};\r\nuse spt_daemon::msg::{\r\n    decode_bytes, encode_bytes, AppliedEvent, EndpointInjected, EndpointInputReq, ExitEvent,\r\n    InputReq, KillReq, OutputEvent, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED,\r\n    KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,\r\n    KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\r\n};\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-b1-{}-{}.sock\", std::process::id(), n)\r\n}\r\n\r\n/// The echo child: reads stdin, writes each line to stdout.\r\nfn echo_spawn_req() -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\r\n    #[cfg(windows)]\r\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\r\n    SpawnReq {\r\n        program,\r\n        args,\r\n        rows: 24,\r\n        cols: 80,\r\n        endpoint: String::new(),\r\n        cwd: None,\r\n        env: Default::default(),\r\n        translation_binary: None,\r\n        adapter: String::new(),\r\n        install_dir: None,\r\n    }\r\n}\r\n\r\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 main_test() {\r\n    let name = unique_name();\r\n    let broker = Broker::bind(&name).expect(\"bind broker\");\r\n\r\n    // The broker accepts one brain and serves it on a handler thread.\r\n    let server_broker = Arc::clone(&broker);\r\n    let server = thread::spawn(move || {\r\n        let handle = server_broker.serve_once().expect(\"accept brain\");\r\n        handle.join().expect(\"handler thread\");\r\n    });\r\n\r\n    // Brain side: connect (retry briefly in case accept lags the bind).\r\n    let mut conn = None;\r\n    for _ in 0..200 {\r\n        match LocalSocketTransport::connect(&name) {\r\n            Ok(c) => {\r\n                conn = Some(c);\r\n                break;\r\n            }\r\n            Err(_) => thread::sleep(Duration::from_millis(10)),\r\n        }\r\n    }\r\n    let mut conn = conn.expect(\"connect to broker\");\r\n    // (No per-read timeout: Windows named pipes reject I/O timeouts. The broker\r\n    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads\r\n    // below return; a genuine stall is caught by the test runner's own timeout.)\r\n\r\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\r\n\r\n    // Spawn the echo child under a broker PTY; read until the `spawned` reply,\r\n    // accumulating any output that interleaves.\r\n    send(\r\n        &mut conn,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(echo_spawn_req()).unwrap(),\r\n    );\r\n    let mut out = Vec::<u8>::new();\r\n    let sid = loop {\r\n        let f = read_frame(&mut conn).expect(\"frame before spawned\");\r\n        match f.kind.as_str() {\r\n            KIND_SPAWNED => {\r\n                break serde_json::from_value::<Spawned>(f.payload)\r\n                    .unwrap()\r\n                    .session_id;\r\n            }\r\n            KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            _ => {}\r\n        }\r\n    };\r\n\r\n    // Inject a line; it must round-trip through the real PTY back as output.\r\n    send(\r\n        &mut conn,\r\n        KIND_INPUT,\r\n        serde_json::to_value(InputReq {\r\n            session_id: sid,\r\n            data_b64: encode_bytes(b\"INJECT-ALPHA\\r\"),\r\n            op_id: None,\r\n            minter: None,\r\n            ack: true,\r\n        })\r\n        .unwrap(),\r\n    );\r\n    let echoed = loop {\r\n        if String::from_utf8_lossy(&out).contains(\"INJECT-ALPHA\") {\r\n            break true;\r\n        }\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(_) => {}\r\n            Err(_) => break false, // recv timeout → echo never arrived\r\n        }\r\n    };\r\n\r\n    // Kill the child and read until the `exit` event.\r\n    send(\r\n        &mut conn,\r\n        KIND_KILL,\r\n        serde_json::to_value(KillReq { session_id: sid }).unwrap(),\r\n    );\r\n    let mut exit: Option<ExitEvent> = None;\r\n    loop {\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_EXIT => {\r\n                exit = Some(serde_json::from_value(f.payload).unwrap());\r\n                break;\r\n            }\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(_) => {}\r\n            Err(_) => break, // timeout\r\n        }\r\n    }\r\n\r\n    // Disconnect: dropping the single Stream closes the fd, so the broker's\r\n    // command loop hits EOF and tears the session down. Joining the handler\r\n    // (via the server thread) lets us assert the teardown happened.\r\n    drop(conn);\r\n    server.join().expect(\"server thread\");\r\n\r\n    assert!(\r\n        echoed,\r\n        \"injected input must round-trip through the PTY: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n    let exit = exit.expect(\"exit event after kill\");\r\n    assert_eq!(exit.session_id, sid);\r\n    assert_eq!(broker.session_count(), 0, \"session torn down on disconnect\");\r\n}\r\n\r\nfn accumulate_output(frame: &Envelope, out: &mut Vec<u8>) {\r\n    let ev: OutputEvent = serde_json::from_value(frame.payload.clone()).unwrap();\r\n    out.extend_from_slice(&decode_bytes(&ev.data_b64).unwrap());\r\n}\r\n\r\n#[test]\r\nfn broker_hosts_pty_child_streams_output_and_accepts_input() {\r\n    main_test();\r\n}\r\n\r\n/// REQ-SEND-SPT-HOSTED / REQ-MSG-IDLE-TRANSLATION-BINARY (ADR-0022 amendment): an\r\n/// inbound message addressed by ENDPOINT ID (`KIND_ENDPOINT_INPUT`) is delivered to\r\n/// an spt-hosted session ONLY through its translation binary. With NO translation\r\n/// binary declared (this echo child), idle delivery has no working binary → the\r\n/// broker acks `delivered=false` and writes NOTHING to the PTY (the caller spools;\r\n/// raw `payload+\\r` was removed as a delivery path — it did not submit on a modern\r\n/// TUI, the silent pseudo-delivery that masked F-019). An inject for an UNKNOWN\r\n/// endpoint likewise replies `delivered=false` and writes nothing. (The happy-path\r\n/// delivery — a real binary's choreography reaching the PTY — is the\r\n/// inject_control_wedge.rs int gate, which spawns the xlate_choreo_fixture binary.)\r\n// [int->REQ-SEND-SPT-HOSTED]\r\n// [int->REQ-MSG-IDLE-TRANSLATION-BINARY]\r\n// [int->REQ-HAZARD-IDLE-SILENT-NONDELIVERY]\r\nfn endpoint_input_test() {\r\n    let name = unique_name();\r\n    let broker = Broker::bind(&name).expect(\"bind broker\");\r\n    let server_broker = Arc::clone(&broker);\r\n    let server = thread::spawn(move || {\r\n        let handle = server_broker.serve_once().expect(\"accept brain\");\r\n        handle.join().expect(\"handler thread\");\r\n    });\r\n\r\n    let mut conn = None;\r\n    for _ in 0..200 {\r\n        match LocalSocketTransport::connect(&name) {\r\n            Ok(c) => {\r\n                conn = Some(c);\r\n                break;\r\n            }\r\n            Err(_) => thread::sleep(Duration::from_millis(10)),\r\n        }\r\n    }\r\n    let mut conn = conn.expect(\"connect to broker\");\r\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\r\n\r\n    // Spawn the echo child LABELLED with an endpoint id (the spawn.endpoint the\r\n    // broker tracks per session — what KIND_ENDPOINT_INPUT resolves against).\r\n    let mut spawn = echo_spawn_req();\r\n    spawn.endpoint = \"wall-b\".to_string();\r\n    send(&mut conn, KIND_SPAWN, serde_json::to_value(spawn).unwrap());\r\n    let mut out = Vec::<u8>::new();\r\n    loop {\r\n        let f = read_frame(&mut conn).expect(\"frame before spawned\");\r\n        match f.kind.as_str() {\r\n            KIND_SPAWNED => break,\r\n            KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    // Endpoint-keyed inject (NO session_id — resolved by endpoint) into a hosted\r\n    // session that declares NO translation binary: idle delivery is\r\n    // translation-binary-ONLY, so there is no working binary → the broker acks\r\n    // delivered=FALSE and writes NOTHING to the PTY (the caller spools).\r\n    send(\r\n        &mut conn,\r\n        KIND_ENDPOINT_INPUT,\r\n        serde_json::to_value(EndpointInputReq {\r\n            endpoint: \"wall-b\".to_string(),\r\n            data_b64: encode_bytes(b\"ENDPOINT-INJECT-BETA\\r\"),\r\n            native: false,\r\n        })\r\n        .unwrap(),\r\n    );\r\n    let mut delivered: Option<bool> = None;\r\n    while delivered.is_none() {\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {\r\n                delivered = Some(\r\n                    serde_json::from_value::<EndpointInjected>(f.payload)\r\n                        .unwrap()\r\n                        .delivered,\r\n                );\r\n            }\r\n            Ok(_) => {}\r\n            Err(_) => break,\r\n        }\r\n    }\r\n    assert_eq!(\r\n        delivered,\r\n        Some(false),\r\n        \"a hosted session with no translation binary acks delivered=false (spool, not raw inject)\"\r\n    );\r\n    assert!(\r\n        !String::from_utf8_lossy(&out).contains(\"ENDPOINT-INJECT-BETA\"),\r\n        \"no working translation binary → nothing is written to the PTY: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n\r\n    // Inject for an UNKNOWN endpoint: delivered=false, nothing written.\r\n    send(\r\n        &mut conn,\r\n        KIND_ENDPOINT_INPUT,\r\n        serde_json::to_value(EndpointInputReq {\r\n            endpoint: \"no-such-endpoint\".to_string(),\r\n            data_b64: encode_bytes(b\"SHOULD-NOT-APPEAR\\r\"),\r\n            native: false,\r\n        })\r\n        .unwrap(),\r\n    );\r\n    let mut miss: Option<bool> = None;\r\n    while miss.is_none() {\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {\r\n                miss = Some(\r\n                    serde_json::from_value::<EndpointInjected>(f.payload)\r\n                        .unwrap()\r\n                        .delivered,\r\n                );\r\n            }\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(_) => {}\r\n            Err(_) => break,\r\n        }\r\n    }\r\n    assert_eq!(miss, Some(false), \"unknown endpoint acks delivered=false\");\r\n    assert!(\r\n        !String::from_utf8_lossy(&out).contains(\"SHOULD-NOT-APPEAR\"),\r\n        \"a miss writes nothing to any PTY\"\r\n    );\r\n\r\n    drop(conn);\r\n    server.join().expect(\"server thread\");\r\n}\r\n\r\n#[test]\r\nfn endpoint_keyed_inject_without_binary_spools_not_pty() {\r\n    endpoint_input_test();\r\n}\r\n\r\n/// REQ-HAZARD-ENV-SUBST: a SpawnReq's `env` map reaches the spawned PTY child's\r\n/// environment — the broker leg of the F-013 fix (a flagless harness gets\r\n/// SPT_ENDPOINT_ID populated, instead of an empty value → seed-by-PPID → zero\r\n/// perch). Spawn a child that ECHOES the injected env var; its output must carry\r\n/// the value.\r\n// [int->REQ-HAZARD-ENV-SUBST]\r\nfn env_inject_test() {\r","truncated":true,"truncatedBy":"lines","totalLines":1221,"totalBytes":12377,"outputLines":323,"outputBytes":12377,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! B1 integration: a [`Broker`] hosts one real PTY child and relays its output\r\n//! and injected input over the versioned local IPC, on both OSes.\r\n//!\r\n//! The child is an echo program (`cat` on Unix, `findstr \".\"` on Windows — the\r\n//! same cross-OS shape spt-term's injection tests use): bytes the brain injects\r\n//! round-trip through the real PTY and come back as `output` events. We then\r\n//! kill it and observe the `exit` event. This exercises the whole B1 contract —\r\n//! spawn under a broker PTY, stream output, accept input, report exit — through\r\n//! the actual named-pipe / UDS transport.\r\n//!\r\n//! The brain is driven **single-threaded**: send a command, then read frames\r\n//! until the expected reply/event arrives (accumulating any interleaved output).\r\n//! interprocess `Stream::split()` shares the underlying fd, so a split client\r\n//! would only signal EOF once *both* halves drop; keeping one `Stream` means\r\n//! dropping it closes the fd, the broker's command loop hits EOF, and it tears\r\n//! the session down deterministically — which we then assert.\r\n// [int->REQ-DAEMON-2]\r\n\r\nuse std::sync::atomic::{AtomicU32, Ordering};\r\nuse std::sync::Arc;\r\nuse std::thread;\r\nuse std::time::Duration;\r\n\r\nuse interprocess::local_socket::Stream;\r\nuse spt_daemon::codec::{read_frame, write_frame};\r\nuse spt_daemon::effect::{EffectKey, Minter};\r\nuse spt_daemon::frame::{Envelope, Role};\r\nuse spt_daemon::msg::{\r\n    decode_bytes, encode_bytes, AppliedEvent, EndpointInjected, EndpointInputReq, ExitEvent,\r\n    InputReq, KillReq, OutputEvent, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED,\r\n    KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,\r\n    KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\r\n};\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-b1-{}-{}.sock\", std::process::id(), n)\r\n}\r\n\r\n/// The echo child: reads stdin, writes each line to stdout.\r\nfn echo_spawn_req() -> SpawnReq {\r\n    #[cfg(unix)]\r\n    let (program, args) = (\"cat\".to_string(), Vec::<String>::new());\r\n    #[cfg(windows)]\r\n    let (program, args) = (\"findstr\".to_string(), vec![\".\".to_string()]);\r\n    SpawnReq {\r\n        program,\r\n        args,\r\n        rows: 24,\r\n        cols: 80,\r\n        endpoint: String::new(),\r\n        cwd: None,\r\n        env: Default::default(),\r\n        translation_binary: None,\r\n        adapter: String::new(),\r\n        install_dir: None,\r\n    }\r\n}\r\n\r\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 main_test() {\r\n    let name = unique_name();\r\n    let broker = Broker::bind(&name).expect(\"bind broker\");\r\n\r\n    // The broker accepts one brain and serves it on a handler thread.\r\n    let server_broker = Arc::clone(&broker);\r\n    let server = thread::spawn(move || {\r\n        let handle = server_broker.serve_once().expect(\"accept brain\");\r\n        handle.join().expect(\"handler thread\");\r\n    });\r\n\r\n    // Brain side: connect (retry briefly in case accept lags the bind).\r\n    let mut conn = None;\r\n    for _ in 0..200 {\r\n        match LocalSocketTransport::connect(&name) {\r\n            Ok(c) => {\r\n                conn = Some(c);\r\n                break;\r\n            }\r\n            Err(_) => thread::sleep(Duration::from_millis(10)),\r\n        }\r\n    }\r\n    let mut conn = conn.expect(\"connect to broker\");\r\n    // (No per-read timeout: Windows named pipes reject I/O timeouts. The broker\r\n    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads\r\n    // below return; a genuine stall is caught by the test runner's own timeout.)\r\n\r\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\r\n\r\n    // Spawn the echo child under a broker PTY; read until the `spawned` reply,\r\n    // accumulating any output that interleaves.\r\n    send(\r\n        &mut conn,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(echo_spawn_req()).unwrap(),\r\n    );\r\n    let mut out = Vec::<u8>::new();\r\n    let sid = loop {\r\n        let f = read_frame(&mut conn).expect(\"frame before spawned\");\r\n        match f.kind.as_str() {\r\n            KIND_SPAWNED => {\r\n                break serde_json::from_value::<Spawned>(f.payload)\r\n                    .unwrap()\r\n                    .session_id;\r\n            }\r\n            KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            _ => {}\r\n        }\r\n    };\r\n\r\n    // Inject a line; it must round-trip through the real PTY back as output.\r\n    send(\r\n        &mut conn,\r\n        KIND_INPUT,\r\n        serde_json::to_value(InputReq {\r\n            session_id: sid,\r\n            data_b64: encode_bytes(b\"INJECT-ALPHA\\r\"),\r\n            op_id: None,\r\n            minter: None,\r\n            ack: true,\r\n        })\r\n        .unwrap(),\r\n    );\r\n    let echoed = loop {\r\n        if String::from_utf8_lossy(&out).contains(\"INJECT-ALPHA\") {\r\n            break true;\r\n        }\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(_) => {}\r\n            Err(_) => break false, // recv timeout → echo never arrived\r\n        }\r\n    };\r\n\r\n    // Kill the child and read until the `exit` event.\r\n    send(\r\n        &mut conn,\r\n        KIND_KILL,\r\n        serde_json::to_value(KillReq { session_id: sid }).unwrap(),\r\n    );\r\n    let mut exit: Option<ExitEvent> = None;\r\n    loop {\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_EXIT => {\r\n                exit = Some(serde_json::from_value(f.payload).unwrap());\r\n                break;\r\n            }\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(_) => {}\r\n            Err(_) => break, // timeout\r\n        }\r\n    }\r\n\r\n    // Disconnect: dropping the single Stream closes the fd, so the broker's\r\n    // command loop hits EOF and tears the session down. Joining the handler\r\n    // (via the server thread) lets us assert the teardown happened.\r\n    drop(conn);\r\n    server.join().expect(\"server thread\");\r\n\r\n    assert!(\r\n        echoed,\r\n        \"injected input must round-trip through the PTY: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n    let exit = exit.expect(\"exit event after kill\");\r\n    assert_eq!(exit.session_id, sid);\r\n    assert_eq!(broker.session_count(), 0, \"session torn down on disconnect\");\r\n}\r\n\r\nfn accumulate_output(frame: &Envelope, out: &mut Vec<u8>) {\r\n    let ev: OutputEvent = serde_json::from_value(frame.payload.clone()).unwrap();\r\n    out.extend_from_slice(&decode_bytes(&ev.data_b64).unwrap());\r\n}\r\n\r\n#[test]\r\nfn broker_hosts_pty_child_streams_output_and_accepts_input() {\r\n    main_test();\r\n}\r\n\r\n/// REQ-SEND-SPT-HOSTED / REQ-MSG-IDLE-TRANSLATION-BINARY (ADR-0022 amendment): an\r\n/// inbound message addressed by ENDPOINT ID (`KIND_ENDPOINT_INPUT`) is delivered to\r\n/// an spt-hosted session ONLY through its translation binary. With NO translation\r\n/// binary declared (this echo child), idle delivery has no working binary → the\r\n/// broker acks `delivered=false` and writes NOTHING to the PTY (the caller spools;\r\n/// raw `payload+\\r` was removed as a delivery path — it did not submit on a modern\r\n/// TUI, the silent pseudo-delivery that masked F-019). An inject for an UNKNOWN\r\n/// endpoint likewise replies `delivered=false` and writes nothing. (The happy-path\r\n/// delivery — a real binary's choreography reaching the PTY — is the\r\n/// inject_control_wedge.rs int gate, which spawns the xlate_choreo_fixture binary.)\r\n// [int->REQ-SEND-SPT-HOSTED]\r\n// [int->REQ-MSG-IDLE-TRANSLATION-BINARY]\r\n// [int->REQ-HAZARD-IDLE-SILENT-NONDELIVERY]\r\nfn endpoint_input_test() {\r\n    let name = unique_name();\r\n    let broker = Broker::bind(&name).expect(\"bind broker\");\r\n    let server_broker = Arc::clone(&broker);\r\n    let server = thread::spawn(move || {\r\n        let handle = server_broker.serve_once().expect(\"accept brain\");\r\n        handle.join().expect(\"handler thread\");\r\n    });\r\n\r\n    let mut conn = None;\r\n    for _ in 0..200 {\r\n        match LocalSocketTransport::connect(&name) {\r\n            Ok(c) => {\r\n                conn = Some(c);\r\n                break;\r\n            }\r\n            Err(_) => thread::sleep(Duration::from_millis(10)),\r\n        }\r\n    }\r\n    let mut conn = conn.expect(\"connect to broker\");\r\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\r\n\r\n    // Spawn the echo child LABELLED with an endpoint id (the spawn.endpoint the\r\n    // broker tracks per session — what KIND_ENDPOINT_INPUT resolves against).\r\n    let mut spawn = echo_spawn_req();\r\n    spawn.endpoint = \"wall-b\".to_string();\r\n    send(&mut conn, KIND_SPAWN, serde_json::to_value(spawn).unwrap());\r\n    let mut out = Vec::<u8>::new();\r\n    loop {\r\n        let f = read_frame(&mut conn).expect(\"frame before spawned\");\r\n        match f.kind.as_str() {\r\n            KIND_SPAWNED => break,\r\n            KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            _ => {}\r\n        }\r\n    }\r\n\r\n    // Endpoint-keyed inject (NO session_id — resolved by endpoint) into a hosted\r\n    // session that declares NO translation binary: idle delivery is\r\n    // translation-binary-ONLY, so there is no working binary → the broker acks\r\n    // delivered=FALSE and writes NOTHING to the PTY (the caller spools).\r\n    send(\r\n        &mut conn,\r\n        KIND_ENDPOINT_INPUT,\r\n        serde_json::to_value(EndpointInputReq {\r\n            endpoint: \"wall-b\".to_string(),\r\n            data_b64: encode_bytes(b\"ENDPOINT-INJECT-BETA\\r\"),\r\n            native: false,\r\n        })\r\n        .unwrap(),\r\n    );\r\n    let mut delivered: Option<bool> = None;\r\n    while delivered.is_none() {\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {\r\n                delivered = Some(\r\n                    serde_json::from_value::<EndpointInjected>(f.payload)\r\n                        .unwrap()\r\n                        .delivered,\r\n                );\r\n            }\r\n            Ok(_) => {}\r\n            Err(_) => break,\r\n        }\r\n    }\r\n    assert_eq!(\r\n        delivered,\r\n        Some(false),\r\n        \"a hosted session with no translation binary acks delivered=false (spool, not raw inject)\"\r\n    );\r\n    assert!(\r\n        !String::from_utf8_lossy(&out).contains(\"ENDPOINT-INJECT-BETA\"),\r\n        \"no working translation binary → nothing is written to the PTY: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n\r\n    // Inject for an UNKNOWN endpoint: delivered=false, nothing written.\r\n    send(\r\n        &mut conn,\r\n        KIND_ENDPOINT_INPUT,\r\n        serde_json::to_value(EndpointInputReq {\r\n            endpoint: \"no-such-endpoint\".to_string(),\r\n            data_b64: encode_bytes(b\"SHOULD-NOT-APPEAR\\r\"),\r\n            native: false,\r\n        })\r\n        .unwrap(),\r\n    );\r\n    let mut miss: Option<bool> = None;\r\n    while miss.is_none() {\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {\r\n                miss = Some(\r\n                    serde_json::from_value::<EndpointInjected>(f.payload)\r\n                        .unwrap()\r\n                        .delivered,\r\n                );\r\n            }\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(_) => {}\r\n            Err(_) => break,\r\n        }\r\n    }\r\n    assert_eq!(miss, Some(false), \"unknown endpoint acks delivered=false\");\r\n    assert!(\r\n        !String::from_utf8_lossy(&out).contains(\"SHOULD-NOT-APPEAR\"),\r\n        \"a miss writes nothing to any PTY\"\r\n    );\r\n\r\n    drop(conn);\r\n    server.join().expect(\"server thread\");\r\n}\r\n\r\n#[test]\r\nfn endpoint_keyed_inject_without_binary_spools_not_pty() {\r\n    endpoint_input_test();\r\n}\r\n\r\n/// REQ-HAZARD-ENV-SUBST: a SpawnReq's `env` map reaches the spawned PTY child's\r\n/// environment — the broker leg of the F-013 fix (a flagless harness gets\r\n/// SPT_ENDPOINT_ID populated, instead of an empty value → seed-by-PPID → zero\r\n/// perch). Spawn a child that ECHOES the injected env var; its output must carry\r\n/// the value.\r\n// [int->REQ-HAZARD-ENV-SUBST]\r\nfn env_inject_test() {\r\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,null,419]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1221,"totalBytes":12377,"outputLines":323,"outputBytes":12377,"shownRange":{"start":1,"end":323},"nextOffset":324}}},"isError":false,"timestamp":1784365558557}}
{"type":"message","id":"1ba043c2","parentId":"07963c7c","timestamp":"2026-07-18T09:05:58.563Z","message":{"role":"toolResult","toolCallId":"call_WLlUgaq0iSqA7SU7MnP5Snza|fc_0ce5441d29b9f05d016a5b41f661d4819bbdb1a08a3d2ba316","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/tests/\n### broker.rs\n 28|use spt_daemon::msg::{\n*29|    decode_bytes, encode_bytes, AppliedEvent, EndpointInjected, EndpointInputReq, ExitEvent,\n*30|    InputReq, KillReq, OutputEvent, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED,\n*31|    KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,\n 32|    KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\n 33|};\n 34|use spt_daemon::transport::{send_hello, LocalSocketTransport};\n...\n 146|    );\n*147|    let mut exit: Option<ExitEvent> = None;\n 148|    loop {\n 149|        match read_frame(&mut conn) {\n*150|            Ok(f) if f.kind == KIND_EXIT => {\n 151|                exit = Some(serde_json::from_value(f.payload).unwrap());\n 152|                break;\n 153|            }\n...\n 176|fn accumulate_output(frame: &Envelope, out: &mut Vec<u8>) {\n*177|    let ev: OutputEvent = serde_json::from_value(frame.payload.clone()).unwrap();\n 178|    out.extend_from_slice(&decode_bytes(&ev.data_b64).unwrap());\n 179|}\n 180|\n...\n 395|            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\n*396|            Ok(f) if f.kind == KIND_EXIT => exited = true,\n 397|            Ok(_) => {}\n 398|            Err(_) => {\n 399|                // After EXIT a run of read errors means the stream is fully drained\n...\n 426|/// REQ-HAZARD-RC-ATTACH-FAILFAST path (c): a subscribe to a session whose child\n*427|/// has EXITED surfaces a prompt Exit (dead-not-reaped → dispatch_subscribe's\n*428|/// try_wait → Exit frame) or a clean \"no such session\" error (already reaped) —\n 429|/// NEVER silence (the infinite-blank hang an attach would otherwise show). Spawn a\n 430|/// child that exits immediately, then poll-subscribe and assert a prompt\n 431|/// non-silent reply.\n...\n 487|\n*488|    // Poll-subscribe: every attempt must yield a prompt Exit or no-such-session\n 489|    // error — never an unanswered (silent) subscribe.\n 490|    let mut answered = false;\n 491|    for _ in 0..100 {\n...\n 505|            match read_frame(&mut conn) {\n*506|                Ok(f) if f.kind == KIND_EXIT || f.kind == KIND_ERROR => {\n 507|                    answered = true;\n 508|                    break;\n 509|                }\n...\n 521|        answered,\n*522|        \"subscribe to a dead session must surface Exit/error promptly, never hang\"\n 523|    );\n 524|}\n 525|\n...\n 1098|            if f.kind == KIND_OUTPUT {\n*1099|                let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n 1100|                dedup.accept(ev.seq, &decode_bytes(&ev.data_b64).unwrap(), &mut ground);\n 1101|            }\n 1102|        }\n...\n 1166|        if f.kind == KIND_OUTPUT {\n*1167|            let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n 1168|            dedup.accept(ev.seq, &decode_bytes(&ev.data_b64).unwrap(), &mut consumer);\n 1169|        }\n 1170|    }\n...\n 1199|        match read_frame(&mut conn) {\n*1200|            Ok(f) if f.kind == KIND_EXIT => break,\n 1201|            Ok(_) => {}\n 1202|            Err(_) => break,\n 1203|        }\n### attach.rs\n 200|\n*201|/// Operator-side viewport: consume attach-stream records, dedup `Output` by\n 202|/// seq, append rendered bytes to `buf`, until `needle` is rendered. Returns\n 203|/// the rendered-through cursor (last accepted seq + 1).\n 204|fn render_until(\n...\n 220|                for rec in decoder.push(&bytes) {\n*221|                    if let AttachRecord::Output { seq, data_b64 } = rec {\n 222|                        if seq < *next_seq {\n 223|                            continue; // replay duplicate — dedup by cursor\n 224|                        }\n...\n 366|    assert_eq!(outcome, AttachServeOutcome::Detached);\n*367|    target.kill_session().expect(\"cleanup kill\");\n 368|}\n 369|\n 370|// [unit->REQ-INST-8] drive an agent('s session) on machine A from machine B:\n...\n 413|    );\n*414|    target.kill_session().expect(\"cleanup kill\");\n 415|}\n 416|\n 417|// [int->REQ-RC-1] LOCAL attach rides the byte-identical pump (M12 W1.5, B1):\n...\n 464|    );\n*465|    target.kill_session().expect(\"cleanup kill\");\n 466|}\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...\n 710|    killer.attach(sid, 0).expect(\"attach for cleanup\");\n*711|    killer.kill_session().expect(\"cleanup kill\");\n 712|}\n 713|\n 714|// [int->REQ-HOST-RUN-2] the broker spawns the PTY child in the requested\n...\n 768|    );\n*769|    brain.kill_session().expect(\"cleanup kill\");\n 770|}\n 771|\n 772|// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\n...\n 831|            }\n*832|            BrokerEvent::Output { seq, bytes, .. } => {\n*833|                let line = ndjson::encode_line(&AttachRecord::Output {\n 834|                    seq,\n 835|                    data_b64: spt_daemon::msg::encode_bytes(&bytes),\n 836|                });\n...\n 921|\n*922|    life2.kill_session().expect(\"cleanup kill\");\n 923|}\n 924|\n 925|// [unit->REQ-REACH-1] remote-drive detection is a daemon fact: serving a\n...\n 1029|    assert!(cleared, \"viewport over → marker cleared (broker re-stamps None)\");\n*1030|    target.kill_session().expect(\"cleanup kill\");\n 1031|}\n 1032|\n 1033|// ── W2.5 controller/viewer model — broker-IPC-level gate evidence ────────────\n*1034|// These exercise the OutputLog controller/viewer fan-out directly over the local\n 1035|// broker IPC: each `attach_as` carries a distinct `by` identity, so the role\n 1036|// matrix (REQ-RCVIEW-1 / REQ-KICK-1) and the isolation hazard\n 1037|// (REQ-HAZARD-VIEWER-ISOLATION) are driven without the two-daemon QUIC harness.\n...\n 1255|        match read_controller_event_resuming(&mut ctrl, sid, 0, Some(\"node-A\")) {\n*1256|            Ok(BrokerEvent::Output { bytes, .. }) => {\n 1257|                got.extend_from_slice(&bytes);\n 1258|                if count(&got, b\"ISOLATED\") >= 1 {\n 1259|                    break;\n...\n 1294|        match brain.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200))) {\n*1295|            Ok(BrokerEvent::Output { bytes, .. }) => {\n 1296|                got.extend_from_slice(&bytes);\n 1297|                if count(&got, needle) >= 1 {\n 1298|                    return true;\n...\n 1336|    // CRASH the controller's serving brain — drop its connection. The broker keeps\n*1337|    // the session, the OutputLog, and the (separate) viewer connection.\n 1338|    drop(ctrl);\n 1339|\n 1340|    // SUCCESSOR re-serves with the SAME identity (node-A) — the brain-restart\n...\n 1397|    driver.send_effect(MintedOp::new(Minter::Rc, 1), b\"SEED_ONE\\r\").expect(\"seed input\");\n*1398|    // Read one Output event to advance the driver's next_seq to 1.\n 1399|    loop {\n 1400|        match driver.read_event().expect(\"driver event\") {\n*1401|            BrokerEvent::Output { .. } => break,\n 1402|            BrokerEvent::Error { message } => panic!(\"driver broker error: {message}\"),\n 1403|            _ => {}\n 1404|        }\n...\n 1455|    let mut succ = succ;\n*1456|    let _ = succ.kill_session();\n 1457|    drop(succ);\n 1458|    let _ = &broker; // hold the broker for the whole test, then drop\n 1459|}\n...\n 1494|        match driver.read_event().expect(\"driver event\") {\n*1495|            BrokerEvent::Output { .. } => break,\n 1496|            BrokerEvent::Error { message } => panic!(\"driver broker error: {message}\"),\n 1497|            _ => {}\n 1498|        }\n\n## spt/src/\n### rc.rs\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 1262|    match outcome {\n*1263|        Ok(PumpEnd::Exited(code)) => {\n 1264|            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n 1265|            Ok(())\n 1266|        }\n...\n 1517|    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n*1518|    // Subscribe our side of the stream so the target's `Output` records surface\n 1519|    // as `NetStreamData` events (the open + Request do not auto-subscribe).\n 1520|    brain\n 1521|        .net_stream_subscribe(stream_id, 0)\n...\n 1701|        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n*1702|        PumpEnd::Exited(_)\n 1703|        | PumpEnd::Detached\n 1704|        | PumpEnd::Displaced(_)\n 1705|        | PumpEnd::Stalled\n...\n 1748|enum PumpEnd {\n*1749|    Exited(Option<i32>),\n 1750|    Detached,\n 1751|    /// A different operator took control (REQ-KICK-1); carries the taker's node.\n 1752|    Displaced(String),\n...\n 1760|    /// returns via the stdin `Detach` path directly) and NOT a session end (an\n*1761|    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\n 1762|    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\n 1763|    /// re-drives. [impl->REQ-RC-RECONNECT]\n 1764|    Severed,\n...\n 1794|/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\n*1795|/// session (empty OutputLog, still painting) MUST pass, so this only catches a\n 1796|/// true hang, never the working fresh-attach.\n 1797|const FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\n 1798|\n...\n 1837|/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n*1838|/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n 1839|#[allow(clippy::too_many_arguments)]\n 1840|fn pump(\n 1841|    brain: &mut Brain,\n...\n 1875|    }\n*1876|    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n 1877|    // drop any `seq` already rendered (the operator render cursor, matching the\n*1878|    // local `OutputLog` replay discipline).\n 1879|    let mut cursor = 0u64;\n 1880|    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n 1881|    // each slice and forward a Resize when it changes (cross-platform without\n...\n 1892|    let mut seen_any = false;\n*1893|    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n 1894|    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n 1895|    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n 1896|    // Any live session replays its buffered ring on attach, so a real attach sets\n...\n 1965|                    match rec {\n*1966|                        AttachRecord::Output { seq, data_b64 } => {\n 1967|                            if seq < cursor {\n 1968|                                continue; // already rendered — re-serve dedup\n 1969|                            }\n...\n 1994|                        }\n*1995|                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n 1996|                        AttachRecord::Displaced { by } => {\n 1997|                            return Ok(PumpEnd::Displaced(by));\n 1998|                        }\n...\n 2159|    // reconnect banner, then post-bounce output from the re-established serve,\n*2160|    // and the viewport still ends HONESTLY (session exit → Exited). Everything\n 2161|    // is in-process against an isolated home: the broker + seed sockets are\n 2162|    // home-tagged (no live-box collision), and the seed channel stays UP through\n 2163|    // the bounce so establish_attach's CONNECT-ONLY is_running() probe stays\n...\n 2326|        };\n*2327|        let pre = wait_for(&viewport, \"pre-bounce ticker output\", &|s| {\n 2328|            String::from_utf8_lossy(s).contains(\"tick\")\n 2329|        });\n 2330|\n...\n 2343|        let banner_needle = b\"Reconnecting to \";\n*2344|        let full = wait_for(&viewport, \"the reconnect banner + post-bounce output\", &|s| {\n 2345|            s.windows(banner_needle.len())\n 2346|                .position(|w| w == banner_needle)\n 2347|                .is_some_and(|i| String::from_utf8_lossy(&s[i..]).contains(\"tick\"))\n...\n 2363|\n*2364|        // Honest finish: the ticker exits on the flag → Exit record → the\n*2365|        // viewport returns Exited (a FINAL end — never re-driven).\n 2366|        std::fs::write(&flag, b\"done\").expect(\"write exit flag\");\n*2367|        host2.kill_session().expect(\"kill the ticker session\");\n 2368|        let outcome = viewport.join().expect(\"viewport thread joins\");\n 2369|        // Reap the successor child (never leak a broker past the test — the\n 2370|        // e2e-leaked-daemon class).\n...\n 2373|        match outcome {\n*2374|            Ok(PumpEnd::Exited(_)) => {}\n 2375|            other => panic!(\"expected the session exit to END the viewport, got {other:?}\"),\n 2376|        }\n 2377|    }"}],"details":{"scopePath":"crates/spt-daemon/tests/broker.rs, crates/spt-daemon/tests/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":55,"fileCount":3,"files":["crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/attach.rs","crates/spt/src/rc.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/broker.rs","count":15},{"path":"crates/spt-daemon/tests/attach.rs","count":20},{"path":"crates/spt/src/rc.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/tests/\n### broker.rs\n   28│use spt_daemon::msg::{\n  *29│    decode_bytes, encode_bytes, AppliedEvent, EndpointInjected, EndpointInputReq, ExitEvent,\n  *30│    InputReq, KillReq, OutputEvent, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED,\n  *31│    KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,\n   32│    KIND_OUTPUT, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,\n   33│};\n   34│use spt_daemon::transport::{send_hello, LocalSocketTransport};\n     │...\n  146│    );\n *147│    let mut exit: Option<ExitEvent> = None;\n  148│    loop {\n  149│        match read_frame(&mut conn) {\n *150│            Ok(f) if f.kind == KIND_EXIT => {\n  151│                exit = Some(serde_json::from_value(f.payload).unwrap());\n  152│                break;\n  153│            }\n     │...\n  176│fn accumulate_output(frame: &Envelope, out: &mut Vec<u8>) {\n *177│    let ev: OutputEvent = serde_json::from_value(frame.payload.clone()).unwrap();\n  178│    out.extend_from_slice(&decode_bytes(&ev.data_b64).unwrap());\n  179│}\n  180│\n     │...\n  395│            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\n *396│            Ok(f) if f.kind == KIND_EXIT => exited = true,\n  397│            Ok(_) => {}\n  398│            Err(_) => {\n  399│                // After EXIT a run of read errors means the stream is fully drained\n     │...\n  426│/// REQ-HAZARD-RC-ATTACH-FAILFAST path (c): a subscribe to a session whose child\n *427│/// has EXITED surfaces a prompt Exit (dead-not-reaped → dispatch_subscribe's\n *428│/// try_wait → Exit frame) or a clean \"no such session\" error (already reaped) —\n  429│/// NEVER silence (the infinite-blank hang an attach would otherwise show). Spawn a\n  430│/// child that exits immediately, then poll-subscribe and assert a prompt\n  431│/// non-silent reply.\n     │...\n  487│\n *488│    // Poll-subscribe: every attempt must yield a prompt Exit or no-such-session\n  489│    // error — never an unanswered (silent) subscribe.\n  490│    let mut answered = false;\n  491│    for _ in 0..100 {\n     │...\n  505│            match read_frame(&mut conn) {\n *506│                Ok(f) if f.kind == KIND_EXIT || f.kind == KIND_ERROR => {\n  507│                    answered = true;\n  508│                    break;\n  509│                }\n     │...\n  521│        answered,\n *522│        \"subscribe to a dead session must surface Exit/error promptly, never hang\"\n  523│    );\n  524│}\n  525│\n     │...\n 1098│            if f.kind == KIND_OUTPUT {\n*1099│                let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n 1100│                dedup.accept(ev.seq, &decode_bytes(&ev.data_b64).unwrap(), &mut ground);\n 1101│            }\n 1102│        }\n     │...\n 1166│        if f.kind == KIND_OUTPUT {\n*1167│            let ev: OutputEvent = serde_json::from_value(f.payload).unwrap();\n 1168│            dedup.accept(ev.seq, &decode_bytes(&ev.data_b64).unwrap(), &mut consumer);\n 1169│        }\n 1170│    }\n     │...\n 1199│        match read_frame(&mut conn) {\n*1200│            Ok(f) if f.kind == KIND_EXIT => break,\n 1201│            Ok(_) => {}\n 1202│            Err(_) => break,\n 1203│        }\n### attach.rs\n  200│\n *201│/// Operator-side viewport: consume attach-stream records, dedup `Output` by\n  202│/// seq, append rendered bytes to `buf`, until `needle` is rendered. Returns\n  203│/// the rendered-through cursor (last accepted seq + 1).\n  204│fn render_until(\n     │...\n  220│                for rec in decoder.push(&bytes) {\n *221│                    if let AttachRecord::Output { seq, data_b64 } = rec {\n  222│                        if seq < *next_seq {\n  223│                            continue; // replay duplicate — dedup by cursor\n  224│                        }\n     │...\n  366│    assert_eq!(outcome, AttachServeOutcome::Detached);\n *367│    target.kill_session().expect(\"cleanup kill\");\n  368│}\n  369│\n  370│// [unit->REQ-INST-8] drive an agent('s session) on machine A from machine B:\n     │...\n  413│    );\n *414│    target.kill_session().expect(\"cleanup kill\");\n  415│}\n  416│\n  417│// [int->REQ-RC-1] LOCAL attach rides the byte-identical pump (M12 W1.5, B1):\n     │...\n  464│    );\n *465│    target.kill_session().expect(\"cleanup kill\");\n  466│}\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     │...\n  710│    killer.attach(sid, 0).expect(\"attach for cleanup\");\n *711│    killer.kill_session().expect(\"cleanup kill\");\n  712│}\n  713│\n  714│// [int->REQ-HOST-RUN-2] the broker spawns the PTY child in the requested\n     │...\n  768│    );\n *769│    brain.kill_session().expect(\"cleanup kill\");\n  770│}\n  771│\n  772│// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain\n     │...\n  831│            }\n *832│            BrokerEvent::Output { seq, bytes, .. } => {\n *833│                let line = ndjson::encode_line(&AttachRecord::Output {\n  834│                    seq,\n  835│                    data_b64: spt_daemon::msg::encode_bytes(&bytes),\n  836│                });\n     │...\n  921│\n *922│    life2.kill_session().expect(\"cleanup kill\");\n  923│}\n  924│\n  925│// [unit->REQ-REACH-1] remote-drive detection is a daemon fact: serving a\n     │...\n 1029│    assert!(cleared, \"viewport over → marker cleared (broker re-stamps None)\");\n*1030│    target.kill_session().expect(\"cleanup kill\");\n 1031│}\n 1032│\n 1033│// ── W2.5 controller/viewer model — broker-IPC-level gate evidence ────────────\n*1034│// These exercise the OutputLog controller/viewer fan-out directly over the local\n 1035│// broker IPC: each `attach_as` carries a distinct `by` identity, so the role\n 1036│// matrix (REQ-RCVIEW-1 / REQ-KICK-1) and the isolation hazard\n 1037│// (REQ-HAZARD-VIEWER-ISOLATION) are driven without the two-daemon QUIC harness.\n     │...\n 1255│        match read_controller_event_resuming(&mut ctrl, sid, 0, Some(\"node-A\")) {\n*1256│            Ok(BrokerEvent::Output { bytes, .. }) => {\n 1257│                got.extend_from_slice(&bytes);\n 1258│                if count(&got, b\"ISOLATED\") >= 1 {\n 1259│                    break;\n     │...\n 1294│        match brain.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200))) {\n*1295│            Ok(BrokerEvent::Output { bytes, .. }) => {\n 1296│                got.extend_from_slice(&bytes);\n 1297│                if count(&got, needle) >= 1 {\n 1298│                    return true;\n     │...\n 1336│    // CRASH the controller's serving brain — drop its connection. The broker keeps\n*1337│    // the session, the OutputLog, and the (separate) viewer connection.\n 1338│    drop(ctrl);\n 1339│\n 1340│    // SUCCESSOR re-serves with the SAME identity (node-A) — the brain-restart\n     │...\n 1397│    driver.send_effect(MintedOp::new(Minter::Rc, 1), b\"SEED_ONE\\r\").expect(\"seed input\");\n*1398│    // Read one Output event to advance the driver's next_seq to 1.\n 1399│    loop {\n 1400│        match driver.read_event().expect(\"driver event\") {\n*1401│            BrokerEvent::Output { .. } => break,\n 1402│            BrokerEvent::Error { message } => panic!(\"driver broker error: {message}\"),\n 1403│            _ => {}\n 1404│        }\n     │...\n 1455│    let mut succ = succ;\n*1456│    let _ = succ.kill_session();\n 1457│    drop(succ);\n 1458│    let _ = &broker; // hold the broker for the whole test, then drop\n 1459│}\n     │...\n 1494│        match driver.read_event().expect(\"driver event\") {\n*1495│            BrokerEvent::Output { .. } => break,\n 1496│            BrokerEvent::Error { message } => panic!(\"driver broker error: {message}\"),\n 1497│            _ => {}\n 1498│        }\n\n## spt/src/\n### rc.rs\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 1262│    match outcome {\n*1263│        Ok(PumpEnd::Exited(code)) => {\n 1264│            let _ = writeln!(stdout, \"\\r\\n[session '{endpoint_id}' ended ({code:?})]\");\n 1265│            Ok(())\n 1266│        }\n     │...\n 1517│    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n*1518│    // Subscribe our side of the stream so the target's `Output` records surface\n 1519│    // as `NetStreamData` events (the open + Request do not auto-subscribe).\n 1520│    brain\n 1521│        .net_stream_subscribe(stream_id, 0)\n     │...\n 1701│        PumpEnd::BrokerGone | PumpEnd::Severed => EndClass::Reconnect,\n*1702│        PumpEnd::Exited(_)\n 1703│        | PumpEnd::Detached\n 1704│        | PumpEnd::Displaced(_)\n 1705│        | PumpEnd::Stalled\n     │...\n 1748│enum PumpEnd {\n*1749│    Exited(Option<i32>),\n 1750│    Detached,\n 1751│    /// A different operator took control (REQ-KICK-1); carries the taker's node.\n 1752│    Displaced(String),\n     │...\n 1760│    /// returns via the stdin `Detach` path directly) and NOT a session end (an\n*1761│    /// `Exit` record precedes a session-death EOF). Pre-B-3 this was mislabeled\n 1762│    /// \"detached — still running\"; it is the SEVERED case the reconnect loop\n 1763│    /// re-drives. [impl->REQ-RC-RECONNECT]\n 1764│    Severed,\n     │...\n 1794│/// session is treated as dead/wedged. Long on purpose — a fresh, mid-init alive\n*1795│/// session (empty OutputLog, still painting) MUST pass, so this only catches a\n 1796│/// true hang, never the working fresh-attach.\n 1797│const FIRST_EVENT_GRACE: Duration = Duration::from_secs(30);\n 1798│\n     │...\n 1837│/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound\n*1838│/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.\n 1839│#[allow(clippy::too_many_arguments)]\n 1840│fn pump(\n 1841│    brain: &mut Brain,\n     │...\n 1875│    }\n*1876│    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;\n 1877│    // drop any `seq` already rendered (the operator render cursor, matching the\n*1878│    // local `OutputLog` replay discipline).\n 1879│    let mut cursor = 0u64;\n 1880│    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size\n 1881│    // each slice and forward a Resize when it changes (cross-platform without\n     │...\n 1892│    let mut seen_any = false;\n*1893│    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n 1894│    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n 1895│    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n 1896│    // Any live session replays its buffered ring on attach, so a real attach sets\n     │...\n 1965│                    match rec {\n*1966│                        AttachRecord::Output { seq, data_b64 } => {\n 1967│                            if seq < cursor {\n 1968│                                continue; // already rendered — re-serve dedup\n 1969│                            }\n     │...\n 1994│                        }\n*1995│                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),\n 1996│                        AttachRecord::Displaced { by } => {\n 1997│                            return Ok(PumpEnd::Displaced(by));\n 1998│                        }\n     │...\n 2159│    // reconnect banner, then post-bounce output from the re-established serve,\n*2160│    // and the viewport still ends HONESTLY (session exit → Exited). Everything\n 2161│    // is in-process against an isolated home: the broker + seed sockets are\n 2162│    // home-tagged (no live-box collision), and the seed channel stays UP through\n 2163│    // the bounce so establish_attach's CONNECT-ONLY is_running() probe stays\n     │...\n 2326│        };\n*2327│        let pre = wait_for(&viewport, \"pre-bounce ticker output\", &|s| {\n 2328│            String::from_utf8_lossy(s).contains(\"tick\")\n 2329│        });\n 2330│\n     │...\n 2343│        let banner_needle = b\"Reconnecting to \";\n*2344│        let full = wait_for(&viewport, \"the reconnect banner + post-bounce output\", &|s| {\n 2345│            s.windows(banner_needle.len())\n 2346│                .position(|w| w == banner_needle)\n 2347│                .is_some_and(|i| String::from_utf8_lossy(&s[i..]).contains(\"tick\"))\n     │...\n 2363│\n*2364│        // Honest finish: the ticker exits on the flag → Exit record → the\n*2365│        // viewport returns Exited (a FINAL end — never re-driven).\n 2366│        std::fs::write(&flag, b\"done\").expect(\"write exit flag\");\n*2367│        host2.kill_session().expect(\"kill the ticker session\");\n 2368│        let outcome = viewport.join().expect(\"viewport thread joins\");\n 2369│        // Reap the successor child (never leak a broker past the test — the\n 2370│        // e2e-leaked-daemon class).\n     │...\n 2373│        match outcome {\n*2374│            Ok(PumpEnd::Exited(_)) => {}\n 2375│            other => panic!(\"expected the session exit to END the viewport, got {other:?}\"),\n 2376│        }\n 2377│    }"},"isError":false,"timestamp":1784365558563}}
{"type":"message","id":"3512f49c","parentId":"1ba043c2","timestamp":"2026-07-18T09:05:58.610Z","message":{"role":"toolResult","toolCallId":"call_22e6KM44B7qHL3ZLNufx9iB0|fc_0ce5441d29b9f05d016a5b41f661c0819bb6fcf9b6ee10414c","toolName":"read","content":[{"type":"text","text":"    #[test]\n    fn stale_generation_detach_never_clears_the_newer_controller() {\n        let (send, _client, _recv) = controller_socket_pair();\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        log.become_controller(Arc::clone(&send), Some(\"op\".into()), 0, 200);\n\n        // A stale worker's late release (gen 100 vs incumbent 200): the\n        // controller survives; the same conn's viewer rows still ptr-clean.\n        log.add_viewer(Arc::clone(&send), 0);\n        assert_eq!(log.viewers.len(), 1);\n        log.detach_if_gen(&send, Some(100));\n        assert!(\n            log.controller.is_some(),\n            \"a stale-generation release must not clear the live controller\"\n        );\n        assert!(log.viewers.is_empty(), \"viewer rows still clean up by ptr\");\n\n        // The matching generation releases normally.\n        log.detach_if_gen(&send, Some(200));\n        assert!(log.controller.is_none(), \"the owning generation's release clears\");\n\n        // None (N-1 / conn cleanup) keeps ptr-identity behavior.\n        log.become_controller(Arc::clone(&send), Some(\"op\".into()), 0, 300);\n        log.detach_if_gen(&send, None);\n        assert!(log.controller.is_none(), \"gen-less detach keeps the legacy ptr clear\");\n    }\n\n    /// #6 CORE (REQ-BROKER-SCREEN-GRID, ADR-0031), the integrated broker seam:\n    /// drive a session through the REAL `OutputLog` with a scripted byte sequence —\n    /// main-screen scrollback, then an alt-screen enter + a TUI paint — then attach a\n    /// cold controller and read the initial batch off a real socket (drain → grid →\n    /// `render_repaint` → `controller_writer` thread → wire). The initial batch must\n    /// be ONE synthesized clean current-screen repaint, NOT the raw ring replayed\n    /// chunk-by-chunk: it paints the alt viewport, switches the client into the alt\n    /// buffer, and does NOT carry the main-screen scrollback that preceded the\n    /// alt-enter (the exact corruption #6/#7/#8 report on `rc` to a running TUI).\n    // [int->REQ-BROKER-SCREEN-GRID]\n    #[test]\n    fn attach_initial_batch_is_a_clean_alt_screen_repaint_not_the_raw_ring() {\n        let (send, mut client, _recv) = controller_socket_pair();\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        // The raw ring holds ALL of these chunks; a raw replay would dump the main\n        // scrollback + the alt-enter into a fresh terminal — the #6 corruption.\n        for chunk in [\n            b\"MAIN-SCROLLBACK-ALPHA\\r\\n\".as_slice(),\n            b\"MAIN-SCROLLBACK-BETA\\r\\n\".as_slice(),\n            b\"\\x1b[?1049h\\x1b[2J\\x1b[H\".as_slice(), // enter alt, clear, home\n            b\"\\x1b[1;1HTUI-TITLE\\x1b[3;1Hviewport-body\".as_slice(),\n        ] {\n            assert!(\n                log.append(chunk).is_none(),\n                \"no controller yet: append returns no controller job\"\n            );\n        }\n\n        // Cold controller attach from seq 0.\n        log.become_controller(Arc::clone(&send), None, 0, 0);\n\n        // The initial batch is ONE output frame carrying the synthesized repaint.\n        let frame = read_frame(&mut client).expect(\"a repaint frame on attach\");\n        assert_eq!(frame.kind, crate::msg::KIND_OUTPUT, \"the initial batch is an output frame\");\n        let ev: crate::msg::OutputEvent =\n            serde_json::from_value(frame.payload).expect(\"output payload\");\n        let repaint = decode_bytes(&ev.data_b64).expect(\"repaint b64 decodes\");\n        let s = String::from_utf8_lossy(&repaint);\n\n        assert!(\n            s.contains(\"TUI-TITLE\") && s.contains(\"viewport-body\"),\n            \"the alt-screen viewport is repainted into the initial batch: {s:?}\"\n        );\n        assert!(\n            !s.contains(\"MAIN-SCROLLBACK-ALPHA\") && !s.contains(\"MAIN-SCROLLBACK-BETA\"),\n            \"the raw main-screen scrollback must NOT ride the alt repaint (the #6 \\\n             corruption a raw-ring replay would produce): {s:?}\"\n        );\n        assert!(\n            s.contains(\"\\x1b[?1049h\"),\n            \"the repaint switches the client into the alt buffer so its own scrollback \\\n             is untouched: {s:?}\"\n        );\n\n        // A SINGLE clean repaint — no raw ring chunks trail it in the initial batch.\n        assert!(\n            try_read_frame(client, Duration::from_millis(300)).is_none(),\n            \"the initial batch is ONE clean repaint, not the raw ring replayed chunk-by-chunk\"\n        );\n\n        log.clear_controller();\n        drop(send);\n        drop(log);\n    }\n\n    // [unit->REQ-CONTROLLER-LIVENESS-REAP] B-2: the KIND_SESSIONS lazy-reap clears a\n    // controller whose WRITER THREAD has EXITED (a severed / keepalive-killed conn\n    // whose writer failed a write, or a detach dropped by a prior B-1 poison wedge) —\n    // so `controller_by`/`has_controller` report honest truth and the existing\n    // converge_perch_stamps clears the stale info.json driven_by stamp. RED-FIRST:\n    // neuter reap_dead_controller to `return false` → the stale controller survives.\n    #[test]\n    fn reap_dead_controller_clears_a_finished_writer() {\n        let (send, _client, _recv) = controller_socket_pair();\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\n        // A controller whose writer thread has already EXITED (models a writer that\n        // returned after a failed socket write on a severed conn).\n        let (tx, _rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\n        let writer = thread::spawn(|| {});\n        for _ in 0..1000 {\n            if writer.is_finished() {\n                break;\n            }\n…\n        }\n…\n    }\n\n[Showing lines 6319-6428 of 6771. Use :6429 to continue]"}],"details":{"truncation":{"content":"    #[test]\r\n    fn stale_generation_detach_never_clears_the_newer_controller() {\r\n        let (send, _client, _recv) = controller_socket_pair();\r\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\r\n        log.become_controller(Arc::clone(&send), Some(\"op\".into()), 0, 200);\r\n\r\n        // A stale worker's late release (gen 100 vs incumbent 200): the\r\n        // controller survives; the same conn's viewer rows still ptr-clean.\r\n        log.add_viewer(Arc::clone(&send), 0);\r\n        assert_eq!(log.viewers.len(), 1);\r\n        log.detach_if_gen(&send, Some(100));\r\n        assert!(\r\n            log.controller.is_some(),\r\n            \"a stale-generation release must not clear the live controller\"\r\n        );\r\n        assert!(log.viewers.is_empty(), \"viewer rows still clean up by ptr\");\r\n\r\n        // The matching generation releases normally.\r\n        log.detach_if_gen(&send, Some(200));\r\n        assert!(log.controller.is_none(), \"the owning generation's release clears\");\r\n\r\n        // None (N-1 / conn cleanup) keeps ptr-identity behavior.\r\n        log.become_controller(Arc::clone(&send), Some(\"op\".into()), 0, 300);\r\n        log.detach_if_gen(&send, None);\r\n        assert!(log.controller.is_none(), \"gen-less detach keeps the legacy ptr clear\");\r\n    }\r\n\r\n    /// #6 CORE (REQ-BROKER-SCREEN-GRID, ADR-0031), the integrated broker seam:\r\n    /// drive a session through the REAL `OutputLog` with a scripted byte sequence —\r\n    /// main-screen scrollback, then an alt-screen enter + a TUI paint — then attach a\r\n    /// cold controller and read the initial batch off a real socket (drain → grid →\r\n    /// `render_repaint` → `controller_writer` thread → wire). The initial batch must\r\n    /// be ONE synthesized clean current-screen repaint, NOT the raw ring replayed\r\n    /// chunk-by-chunk: it paints the alt viewport, switches the client into the alt\r\n    /// buffer, and does NOT carry the main-screen scrollback that preceded the\r\n    /// alt-enter (the exact corruption #6/#7/#8 report on `rc` to a running TUI).\r\n    // [int->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn attach_initial_batch_is_a_clean_alt_screen_repaint_not_the_raw_ring() {\r\n        let (send, mut client, _recv) = controller_socket_pair();\r\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\r\n        // The raw ring holds ALL of these chunks; a raw replay would dump the main\r\n        // scrollback + the alt-enter into a fresh terminal — the #6 corruption.\r\n        for chunk in [\r\n            b\"MAIN-SCROLLBACK-ALPHA\\r\\n\".as_slice(),\r\n            b\"MAIN-SCROLLBACK-BETA\\r\\n\".as_slice(),\r\n            b\"\\x1b[?1049h\\x1b[2J\\x1b[H\".as_slice(), // enter alt, clear, home\r\n            b\"\\x1b[1;1HTUI-TITLE\\x1b[3;1Hviewport-body\".as_slice(),\r\n        ] {\r\n            assert!(\r\n                log.append(chunk).is_none(),\r\n                \"no controller yet: append returns no controller job\"\r\n            );\r\n        }\r\n\r\n        // Cold controller attach from seq 0.\r\n        log.become_controller(Arc::clone(&send), None, 0, 0);\r\n\r\n        // The initial batch is ONE output frame carrying the synthesized repaint.\r\n        let frame = read_frame(&mut client).expect(\"a repaint frame on attach\");\r\n        assert_eq!(frame.kind, crate::msg::KIND_OUTPUT, \"the initial batch is an output frame\");\r\n        let ev: crate::msg::OutputEvent =\r\n            serde_json::from_value(frame.payload).expect(\"output payload\");\r\n        let repaint = decode_bytes(&ev.data_b64).expect(\"repaint b64 decodes\");\r\n        let s = String::from_utf8_lossy(&repaint);\r\n\r\n        assert!(\r\n            s.contains(\"TUI-TITLE\") && s.contains(\"viewport-body\"),\r\n            \"the alt-screen viewport is repainted into the initial batch: {s:?}\"\r\n        );\r\n        assert!(\r\n            !s.contains(\"MAIN-SCROLLBACK-ALPHA\") && !s.contains(\"MAIN-SCROLLBACK-BETA\"),\r\n            \"the raw main-screen scrollback must NOT ride the alt repaint (the #6 \\\r\n             corruption a raw-ring replay would produce): {s:?}\"\r\n        );\r\n        assert!(\r\n            s.contains(\"\\x1b[?1049h\"),\r\n            \"the repaint switches the client into the alt buffer so its own scrollback \\\r\n             is untouched: {s:?}\"\r\n        );\r\n\r\n        // A SINGLE clean repaint — no raw ring chunks trail it in the initial batch.\r\n        assert!(\r\n            try_read_frame(client, Duration::from_millis(300)).is_none(),\r\n            \"the initial batch is ONE clean repaint, not the raw ring replayed chunk-by-chunk\"\r\n        );\r\n\r\n        log.clear_controller();\r\n        drop(send);\r\n        drop(log);\r\n    }\r\n\r\n    // [unit->REQ-CONTROLLER-LIVENESS-REAP] B-2: the KIND_SESSIONS lazy-reap clears a\r\n    // controller whose WRITER THREAD has EXITED (a severed / keepalive-killed conn\r\n    // whose writer failed a write, or a detach dropped by a prior B-1 poison wedge) —\r\n    // so `controller_by`/`has_controller` report honest truth and the existing\r\n    // converge_perch_stamps clears the stale info.json driven_by stamp. RED-FIRST:\r\n    // neuter reap_dead_controller to `return false` → the stale controller survives.\r\n    #[test]\r\n    fn reap_dead_controller_clears_a_finished_writer() {\r\n        let (send, _client, _recv) = controller_socket_pair();\r\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\r\n        // A controller whose writer thread has already EXITED (models a writer that\r\n        // returned after a failed socket write on a severed conn).\r\n        let (tx, _rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\r\n        let writer = thread::spawn(|| {});\r\n        for _ in 0..1000 {\r\n            if writer.is_finished() {\r\n                break;\r\n            }\r","truncated":true,"truncatedBy":"lines","totalLines":453,"totalBytes":5766,"outputLines":110,"outputBytes":5766,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"    #[test]\r\n    fn stale_generation_detach_never_clears_the_newer_controller() {\r\n        let (send, _client, _recv) = controller_socket_pair();\r\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\r\n        log.become_controller(Arc::clone(&send), Some(\"op\".into()), 0, 200);\r\n\r\n        // A stale worker's late release (gen 100 vs incumbent 200): the\r\n        // controller survives; the same conn's viewer rows still ptr-clean.\r\n        log.add_viewer(Arc::clone(&send), 0);\r\n        assert_eq!(log.viewers.len(), 1);\r\n        log.detach_if_gen(&send, Some(100));\r\n        assert!(\r\n            log.controller.is_some(),\r\n            \"a stale-generation release must not clear the live controller\"\r\n        );\r\n        assert!(log.viewers.is_empty(), \"viewer rows still clean up by ptr\");\r\n\r\n        // The matching generation releases normally.\r\n        log.detach_if_gen(&send, Some(200));\r\n        assert!(log.controller.is_none(), \"the owning generation's release clears\");\r\n\r\n        // None (N-1 / conn cleanup) keeps ptr-identity behavior.\r\n        log.become_controller(Arc::clone(&send), Some(\"op\".into()), 0, 300);\r\n        log.detach_if_gen(&send, None);\r\n        assert!(log.controller.is_none(), \"gen-less detach keeps the legacy ptr clear\");\r\n    }\r\n\r\n    /// #6 CORE (REQ-BROKER-SCREEN-GRID, ADR-0031), the integrated broker seam:\r\n    /// drive a session through the REAL `OutputLog` with a scripted byte sequence —\r\n    /// main-screen scrollback, then an alt-screen enter + a TUI paint — then attach a\r\n    /// cold controller and read the initial batch off a real socket (drain → grid →\r\n    /// `render_repaint` → `controller_writer` thread → wire). The initial batch must\r\n    /// be ONE synthesized clean current-screen repaint, NOT the raw ring replayed\r\n    /// chunk-by-chunk: it paints the alt viewport, switches the client into the alt\r\n    /// buffer, and does NOT carry the main-screen scrollback that preceded the\r\n    /// alt-enter (the exact corruption #6/#7/#8 report on `rc` to a running TUI).\r\n    // [int->REQ-BROKER-SCREEN-GRID]\r\n    #[test]\r\n    fn attach_initial_batch_is_a_clean_alt_screen_repaint_not_the_raw_ring() {\r\n        let (send, mut client, _recv) = controller_socket_pair();\r\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\r\n        // The raw ring holds ALL of these chunks; a raw replay would dump the main\r\n        // scrollback + the alt-enter into a fresh terminal — the #6 corruption.\r\n        for chunk in [\r\n            b\"MAIN-SCROLLBACK-ALPHA\\r\\n\".as_slice(),\r\n            b\"MAIN-SCROLLBACK-BETA\\r\\n\".as_slice(),\r\n            b\"\\x1b[?1049h\\x1b[2J\\x1b[H\".as_slice(), // enter alt, clear, home\r\n            b\"\\x1b[1;1HTUI-TITLE\\x1b[3;1Hviewport-body\".as_slice(),\r\n        ] {\r\n            assert!(\r\n                log.append(chunk).is_none(),\r\n                \"no controller yet: append returns no controller job\"\r\n            );\r\n        }\r\n\r\n        // Cold controller attach from seq 0.\r\n        log.become_controller(Arc::clone(&send), None, 0, 0);\r\n\r\n        // The initial batch is ONE output frame carrying the synthesized repaint.\r\n        let frame = read_frame(&mut client).expect(\"a repaint frame on attach\");\r\n        assert_eq!(frame.kind, crate::msg::KIND_OUTPUT, \"the initial batch is an output frame\");\r\n        let ev: crate::msg::OutputEvent =\r\n            serde_json::from_value(frame.payload).expect(\"output payload\");\r\n        let repaint = decode_bytes(&ev.data_b64).expect(\"repaint b64 decodes\");\r\n        let s = String::from_utf8_lossy(&repaint);\r\n\r\n        assert!(\r\n            s.contains(\"TUI-TITLE\") && s.contains(\"viewport-body\"),\r\n            \"the alt-screen viewport is repainted into the initial batch: {s:?}\"\r\n        );\r\n        assert!(\r\n            !s.contains(\"MAIN-SCROLLBACK-ALPHA\") && !s.contains(\"MAIN-SCROLLBACK-BETA\"),\r\n            \"the raw main-screen scrollback must NOT ride the alt repaint (the #6 \\\r\n             corruption a raw-ring replay would produce): {s:?}\"\r\n        );\r\n        assert!(\r\n            s.contains(\"\\x1b[?1049h\"),\r\n            \"the repaint switches the client into the alt buffer so its own scrollback \\\r\n             is untouched: {s:?}\"\r\n        );\r\n\r\n        // A SINGLE clean repaint — no raw ring chunks trail it in the initial batch.\r\n        assert!(\r\n            try_read_frame(client, Duration::from_millis(300)).is_none(),\r\n            \"the initial batch is ONE clean repaint, not the raw ring replayed chunk-by-chunk\"\r\n        );\r\n\r\n        log.clear_controller();\r\n        drop(send);\r\n        drop(log);\r\n    }\r\n\r\n    // [unit->REQ-CONTROLLER-LIVENESS-REAP] B-2: the KIND_SESSIONS lazy-reap clears a\r\n    // controller whose WRITER THREAD has EXITED (a severed / keepalive-killed conn\r\n    // whose writer failed a write, or a detach dropped by a prior B-1 poison wedge) —\r\n    // so `controller_by`/`has_controller` report honest truth and the existing\r\n    // converge_perch_stamps clears the stale info.json driven_by stamp. RED-FIRST:\r\n    // neuter reap_dead_controller to `return false` → the stale controller survives.\r\n    #[test]\r\n    fn reap_dead_controller_clears_a_finished_writer() {\r\n        let (send, _client, _recv) = controller_socket_pair();\r\n        let mut log = OutputLog::new(1, DEFAULT_LOG_CHUNKS, String::new(), (24, 80));\r\n        // A controller whose writer thread has already EXITED (models a writer that\r\n        // returned after a failed socket write on a severed conn).\r\n        let (tx, _rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);\r\n        let writer = thread::spawn(|| {});\r\n        for _ in 0..1000 {\r\n            if writer.is_finished() {\r\n                break;\r\n            }\r\n…\n        }\n…\n    }","startLine":6319,"lineNumbers":[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,6354,6355,6356,6357,6358,6359,6360,6361,6362,6363,6364,6365,6366,6367,6368,6369,6370,6371,6372,6373,6374,6375,6376,6377,6378,6379,6380,6381,6382,6383,6384,6385,6386,6387,6388,6389,6390,6391,6392,6393,6394,6395,6396,6397,6398,6399,6400,6401,6402,6403,6404,6405,6406,6407,6408,6409,6410,6411,6412,6413,6414,6415,6416,6417,6418,6419,6420,6421,6422,6423,6424,6425,6426,6427,6428,null,6430,null,6454]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":6771,"totalBytes":5766,"outputLines":110,"outputBytes":5766,"shownRange":{"start":6319,"end":6428},"nextOffset":6429}}},"isError":false,"timestamp":1784365558610}}
{"type":"message","id":"0b2c1502","parentId":"3512f49c","timestamp":"2026-07-18T09:05:58.671Z","message":{"role":"toolResult","toolCallId":"call_GknnMtOcwF4mrhhyWr7Ufjc9|fc_0ce5441d29b9f05d016a5b41f661c8819baf582d9ca0092a19","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/tests/\n## brain_decouple.rs\n 340|\n*341|    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 342|    if let Some(pid) = broker.session_pid(sid) {\n 343|        kill_pid(pid);\n 344|    }\n...\n 411|// deadline; the test captures plain values (bools / Options / Instants) as it\n*412|// goes and only asserts AFTER teardown, so it can never hang even on broken code\n 413|// — worst case it fails an assert after its bounded windows elapse.\n 414|// ─────────────────────────────────────────────────────────────────────────────\n 415|\n...\n 754|\n*755|    // ── Teardown BEFORE asserting (existing test's pattern): kill the PTY child and\n 756|    //    stop the broker so a failing assert leaves no orphan child. Drop the\n 757|    //    black-holed Stream only now (the very end). ──\n 758|    if let Some(pid) = broker.session_pid(sid) {\n## broker.rs\n 7|//! kill it and observe the `exit` event. This exercises the whole B1 contract —\n*8|//! spawn under a broker PTY, stream output, accept input, report exit — through\n 9|//! the actual named-pipe / UDS transport.\n 10|//!\n 11|//! The brain is driven **single-threaded**: send a command, then read frames\n...\n 90|    // (No per-read timeout: Windows named pipes reject I/O timeouts. The broker\n*91|    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads\n 92|    // below return; a genuine stall is caught by the test runner's own timeout.)\n 93|\n 94|    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n...\n 161|    // command loop hits EOF and tears the session down. Joining the handler\n*162|    // (via the server thread) lets us assert the teardown happened.\n 163|    drop(conn);\n 164|    server.join().expect(\"server thread\");\n 165|\n...\n 376|    // $SPT_ENDPOINT_ID` → exits) prints once then dies, so on a fast/quiet box the\n*377|    // EXIT notification and a transient read Err can both arrive BEFORE the OUTPUT\n 378|    // frame is delivered. The old loop broke on the first `Err` OR on `EXIT`, so it\n 379|    // bailed with out=\"\" (a rotating contention victim, then an 18ms fast-fail on\n 380|    // the serial Phase-B box). Instead RETRY on Err (every other read loop in this\n 381|    // file does `Err => sleep(10ms)`), bound by a wall-clock deadline, and — once the\n*382|    // child has EXITed — keep draining briefly so a trailing OUTPUT frame still lands\n 383|    // before we give up.\n 384|    let mut out = Vec::<u8>::new();\n 385|    let mut got = false;\n...\n 399|                // After EXIT a run of read errors means the stream is fully drained\n*400|                // (no trailing output is coming) — stop. Before EXIT the frame just\n 401|                // isn't here yet, so wait and retry.\n 402|                if exited {\n 403|                    post_exit_drains += 1;\n...\n 1172|    // Byte-exactness: the consumer's ACCEPTED stream (live prefix + post-subscribe\n*1173|    // tail) is exactly-once — every marker appears, none duplicated into the\n 1174|    // accepted record by the replay (dedup held). The full accepted seq vector is\n 1175|    // strictly increasing (re-affirms monotonic, independent of the in-loop assert).\n 1176|    assert!(\n...\n 1191|\n*1192|    // Clean teardown: kill the child, drain to the exit, drop the conn.\n 1193|    send(\n 1194|        &mut conn,\n 1195|        KIND_KILL,\n## conn_blackhole_lifecycle.rs\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 55|//! VIEWER attach (not just a controller take) proven to receive output.\n...\n 76|//! this test rides a bounded thread+channel or a polled deadline; the test\n*77|//! captures plain values as it goes and only asserts AFTER teardown, so it can\n 78|//! never hang even on broken code.\n 79|#![cfg(windows)]\n 80|\n...\n 582|\n*583|    // ── Teardown BEFORE asserting (so a failing assert leaves no orphan child):\n 584|    //    kill both PTY children, stop the broker, drop the black-holed Stream. ──\n 585|    if let Some(pid) = broker.session_pid(sid_b) {\n 586|        kill_pid(pid);\n## control_stamp_lifetime.rs\n 10|//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child\n*11|//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n 12|//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n 13|//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n 14|//! path dropped the OutputLog with the controller slot still occupied, so the\n...\n 20|//!\n*21|//! The exit-waiter calls `OutputLog::stamp_reaped()` on reap, which clears all\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## daemon_e2e.rs\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 24|//!\n 25|//! The child is the same cross-OS echo program the other daemon tests use (`cat` /\n...\n 89|// A stdin→stdout passthrough so the echo-commune summarizer writes a real drop\n*90|// file — lets the signoff assert the echo (3.3) actually ran before teardown.\n 91|#[cfg(windows)]\n 92|const PASSTHROUGH: &str = \"sort\";\n 93|#[cfg(unix)]\n...\n 286|\n*287|    // ── 5. graceful-signoff: echo BEFORE teardown (3.3), ready marker removed ───\n*288|    // Seed the Self's ready marker (graceful teardown removes it).\n 289|    let ready = perch::resolve_ready_file(id, ParentHint::Infer);\n 290|    std::fs::create_dir_all(ready.parent().unwrap()).unwrap();\n 291|    std::fs::write(&ready, \"\").unwrap();\n...\n 299|        drops.path().join(\"doyle-commune.md\").exists(),\n*300|        \"the echo wrote a final-delta commune before teardown\"\n 301|    );\n*302|    assert!(!ready.exists(), \"teardown removed the Self's ready marker\");\n 303|\n*304|    // The hosted Psyche is marked offline on teardown (the online-stamp\n 305|    // counterpart). mark_offline is a definitive-death observation, so it also\n 306|    // terminal-normalizes the rest intent (ADR-0041 decision 3).\n 307|    // [unit->REQ-REST-TERMINAL-NORMALIZE]\n## dispatch.rs\n 915|    .expect(\"relink round trip\");\n*916|    let ShellLinkRequestOutcome::Ok(detail) = &out else {\n 917|        panic!(\"{out:?}\")\n 918|    };\n*919|    assert!(detail.contains(&format!(\"relinked {id}\")), \"{detail}\");\n 920|    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, \"doyle-d8c\", &id);\n 921|    let token =\n 922|        spt_daemon::shellhost::read_link_token(&shell_perch).expect(\"fresh token parked at B\");\n...\n 936|    .expect(\"cmd round trip\");\n*937|    let ShellLinkRequestOutcome::Ok(detail) = &out else {\n 938|        panic!(\"{out:?}\")\n 939|    };\n*940|    assert!(detail.contains(\"spooled op=note\"), \"{detail}\");\n 941|    let rows = spool::peek_all_at(&shell_perch).expect(\"spool\");\n 942|    assert_eq!(rows.len(), 1, \"exactly one frame spooled at B\");\n 943|    let key = spt_daemon::shellhost::link_key(&token);\n## driven_by_selfheal.rs\n 385|\n*386|    // Teardown: reap the PTY child (scoped) so the live session never leaks.\n 387|    if let Some(pid) = broker.session_pid(sid) {\n 388|        kill_pid(pid);\n 389|    }\n## endpoint_survival.rs\n 134|\n*135|    // Best-effort teardown: kill the long-lived harness child before asserting.\n 136|    if let Some(sid) = survivor_session_id(&name) {\n 137|        if let Some(pid) = broker.session_pid(sid) {\n 138|            kill_pid(pid);\n...\n 160|\n*161|/// The broker session id hosting the \"survivor\" endpoint, if any (for teardown).\n 162|fn survivor_session_id(name: &str) -> Option<u64> {\n 163|    let mut brain = Brain::cold_start(name, now_ms()).ok()?;\n 164|    brain\n## false_promote.rs\n 350|\n*351|    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 352|    stop.store(true, Ordering::Relaxed);\n 353|    if let Some(pid) = broker.session_pid(sid) {\n 354|        kill_pid(pid);\n## idempotent.rs\n 181|        Some((6, Window::BeforeEffect)),\n*182|        None, // a clean life delivers the tail\n 183|    ];\n 184|\n 185|    let mut state = brain.snapshot();\n## inject_control_wedge.rs\n 147|\n*148|/// Join a teardown helper thread, but NEVER ride a permanently-parked helper to\n 149|/// the nextest slow-timeout backstop (240s). On the FAIL/wedge path a helper can\n 150|/// be parked forever inside a blocking socket op — e.g. the driver's `send_effect`\n 151|/// on a wedged broker never returns to observe `stop` — so a bare `.join()` would\n...\n 153|/// gate result (`subscribed`/`got_output`) is already captured by the 30s watchdog\n*154|/// BEFORE teardown, so bounding the join lets a real product wedge fail FAST on the\n 155|/// assertion (~watchdog time) instead of timing out. A timed-out helper is\n 156|/// abandoned — it dies at process exit, exactly like the KH-7.6 abandoned\n 157|/// pump-ipc-reader threads this file already relies on. Returns true iff it joined.\n...\n 167|/// Read `Broker::session_pid` under a bound. The pid is only used to best-effort\n*168|/// kill the flood child during teardown, but `session_pid` locks the broker's\n 169|/// `sessions` Mutex — if a wedged per-conn handler is holding that lock the read\n*170|/// blocks and rides teardown to the 240s backstop. Probe it on a detached thread\n 171|/// (the broker is an `Arc`, so the clone keeps it alive) and give up on timeout;\n 172|/// a missed child-kill is harmless, the process-per-test exit reaps it.\n 173|fn session_pid_bounded(broker: &Arc<Broker>, sid: u64, dur: Duration) -> Option<u32> {\n...\n 210|/// A QUIET, long-lived child for the w5_a2 IDLE characterization (doyle gate\n*211|/// ruling, Option B): it produces NO output and does NOT exit, so the session is\n 212|/// GENUINELY idle for the read window. (`flood_spawn_req` floods through the read,\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...\n 650|        // Sustain journaled input: keep ops in flight (and, on Unix, parked under\n*651|        // the journal lock) until teardown. Each send_effect writes a frame; the\n 652|        // pump reader drains replies concurrently so the write never deadlocks.\n 653|        while !driver_stop.load(std::sync::atomic::Ordering::Relaxed) {\n 654|            if w.send_effect(MintedOp::new(Minter::Rc, op), &chunk).is_err() {\n...\n 811|    // Watchdog: a hard wedge surfaces as a recv timeout (a test FAILURE), never a\n*812|    // hang. The attacher reports its result before any blocking teardown, so this\n 813|    // recv is the hard ceiling on the whole gate. Deadline is env-tunable\n 814|    // (attach_gate_watchdog: default 30s tight, CI-generous): a real wedge blocks\n 815|    // FOREVER so it trips any finite deadline — the knob only grants headroom for a\n...\n 843|    stop.store(true, std::sync::atomic::Ordering::Relaxed);\n*844|    // Bound the teardown joins: on a REAL facet-B wedge the driver stays parked in\n 845|    // `send_effect` and the attacher in its subscribe — neither observes `stop`, so\n 846|    // a bare `.join()` would ride to the 240s nextest backstop and MASK the verdict\n 847|    // (the symptom doyle's kitsubito reliability run hit). The gate result is already\n...\n 859|\n*860|    // ── PROCESS-CLEAN-EXIT teardown (test-robustness, not the gate). ──\n 861|    // Each `cold_start_pump` Brain (the driver's `w`, the attacher's `operator`)\n 862|    // spawns a DETACHED `pump-ipc-reader` thread parked in a BLOCKING named-pipe\n 863|    // `read_frame` on its RecvHalf. Joining the helper threads drops those Brains,\n...\n 868|    // On Windows, if the test returns before that chain completes, a reader thread\n*869|    // is still parked inside a named-pipe read syscall at process teardown, and a\n 870|    // thread blocked in that syscall on a half-closed handle aborts the WHOLE\n 871|    // process with exit 0xffffffff (the flaky ~50% abort under `--test-threads=1`,\n 872|    // where the next test / process exit races the chain). FIX: after the Brains\n...\n 875|    // moves on. Verified by `PUMP_IPC_READER: exited` printing within this window.\n*876|    // This is purely teardown hygiene; the assertions below are unchanged.\n 877|    thread::sleep(Duration::from_millis(750));\n 878|\n 879|    eprintln!(\n...\n 1301|        }\n*1302|        controller // hand it back so it is dropped only after teardown\n 1303|    });\n 1304|\n 1305|    // ── Deliver an inbound message as a real `<EVENT…>` envelope via KIND_ENDPOINT_INPUT.\n...\n 1331|\n*1332|    // Teardown: reap the PTY child + the controller drainer.\n 1333|    if let Some(pid) = broker.session_pid(sid) {\n 1334|        kill_pid(pid);\n 1335|    }\n...\n 1603|    // it: the `xlate_choreo_fixture` process is a child of THIS test process. We\n*1604|    // confirm it is live before teardown, then assert it is gone after.\n 1605|    let xlate_pid = find_xlate_child_pid().expect(\"translation child should be running\");\n 1606|    assert!(\n 1607|        pid_alive(xlate_pid),\n*1608|        \"the translation child must be live before teardown (sanity)\"\n 1609|    );\n 1610|\n 1611|    // Tear the session down: kill the PTY child → the exit waiter reaps the session\n...\n 1664|        \"doyle gate #2 (no-zombie): the translation child must be terminated + reaped \\\n*1665|         within the bounded reap window after session-down / broker teardown — a \\\n 1666|         session-down must never leak a translation child\"\n 1667|    );\n 1668|}\n...\n 2307|    // serve_attach / become_controller) with a QUIET long-lived child\n*2308|    // (`quiet_spawn_req`, no output, never exits, killed only at teardown — no mid-read\n 2309|    // kill, no Windows reap-during-read flake).\n 2310|    init_wedge_home();\n 2311|    let dir = tempfile::tempdir().expect(\"tempdir\");\n...\n 2334|    // read with NO mid-read kill — which also removes the Windows reap-during-read\n*2335|    // flake the old flood-then-kill rig hit. The child is killed only at teardown.\n 2336|    let mut spawner = connect_retry(&name);\n 2337|    let sid = spawner\n 2338|        .spawn_session(quiet_spawn_req(endpoint))\n...\n 2390|\n*2391|    // Capture the child pid for TEARDOWN only — the quiet child is NEVER killed\n 2392|    // mid-read (it stays live + silent through the read window, so the idle slot is\n 2393|    // faithfully observable and the broker keeps the session row).\n 2394|    let child_pid = _broker.session_pid(sid);\n...\n 2401|\n*2402|    // Teardown.\n 2403|    stop.store(true, Ordering::Relaxed);\n 2404|    if let Some(pid) = child_pid {\n 2405|        kill_pid(pid);\n## input_ack_deadlock.rs\n 514|\n*515|    // ── Teardown: both verdicts are already captured off the watchdog'd channels,\n 516|    //    so the GATE never depends on a helper thread terminating. Kill the echo\n 517|    //    child (closes the PTY → unwinds the abandoned serve_attach threads + their\n 518|    //    target conns), then DETACH the helper handles (do NOT join — a serve_attach\n...\n 527|    // Settle so the child-kill close chain reaches the abandoned serve threads before\n*528|    // the process exits (teardown hygiene; the gate is already decided above).\n 529|    thread::sleep(Duration::from_millis(750));\n 530|\n 531|    eprintln!(\n## legacy_resident_sweep_e2e.rs\n 49|}\n*50|// A trailing shell builtin (`:`) keeps the shell RESIDENT (no tail-exec-replace by\n 51|// `sleep`) so /proc/<pid>/cmdline retains the full `-c` string incl the marker.\n 52|#[cfg(unix)]\n 53|fn spawn_marked(marker: &str) -> std::process::Child {\n...\n 183|\n*184|    // Teardown: tree-reap the control (its shell + the long-lived ping/sleep grandchild)\n 185|    // by pid — the mock tree was already reaped by the sweep. Own spawned pid tree only,\n 186|    // never a broad reap.\n 187|    spt_store::proc::kill_pid_tree(control_pid);\n## pairjoin.rs\n 100|    assert!(\n*101|        reply.detail.contains(\"expired\") || reply.detail.contains(\"re-run\"),\n 102|        \"the failure guides the operator to re-run the join, got: {}\",\n*103|        reply.detail\n 104|    );\n 105|}\n 106|\n## psyche_context_file_e2e.rs\n 14|//! non-empty stdout. The test asserts the proof holds the WHOLE ~40KB mind (both a\n*15|//! head AND a tail marker present, length >= the seed) — proving the mind reached the\n 16|//! role via the FILE, unbroken/untruncated, and never rode argv.\n 17|//!\n 18|//! RED-FIRST (guard-revert, documented): in\n...\n 38|/// The ~40KB preload the FreshWithPreload turn composes (a real doyle-scale mind).\n*39|/// HEAD + TAIL markers bracket a 40KB filler so a TRUNCATED delivery (the argv-cap\n*40|/// brick) loses the tail — a full-file delivery keeps both.\n 41|const ROLE_HEAD: &str = \"ROLE-HEAD-MARKER-7f3a\";\n*42|const ROLE_TAIL: &str = \"ROLE-TAIL-MARKER-9b2c\";\n 43|fn big_role() -> String {\n*44|    format!(\"{ROLE_HEAD}\\n{}\\n{ROLE_TAIL}\", \"x\".repeat(40_000))\n 45|}\n 46|\n 47|#[cfg(windows)]\n...\n 182|    let on_disk = std::fs::read_to_string(&context_file).unwrap();\n*183|    assert!(on_disk.contains(ROLE_HEAD) && on_disk.contains(ROLE_TAIL));\n 184|\n 185|    // THE LOAD-BEARING CLAIM: the FULL ~40KB mind reached the role VIA THE FILE — both\n*186|    // the head AND the tail survived (a truncated argv delivery would drop the tail),\n 187|    // and the proof (the role's own copy of {psyche_context_file}) is >= the seed size.\n 188|    // [int->REQ-PSYCHE-CONTEXT-FILE-INDIRECTION]\n 189|    assert!(\n...\n 193|    assert!(\n*194|        proof_body.contains(ROLE_TAIL),\n*195|        \"the role read the mind's TAIL from the file — the full body arrived UNTRUNCATED \\\n 196|         (a mind-on-argv path would brick/truncate at the win32 32k cap)\"\n 197|    );\n 198|    assert!(\n## psyche_residency_expectation_e2e.rs\n 36|//! RED-FIRST (guard-revert, documented): re-introduce a destructive `set.stop_host(id)`\n*37|//! / OFFLINE-stamp on the turn-failure arm (the exact v0.13.2-shape teardown W3\n 38|//! deleted) → the endpoint un-hosts, its status flips offline, and its ready marker is\n 39|//! removed → every \"stays deliverable / stays hosted\" assertion below goes RED.\n 40|//!\n...\n 234|    // (1) still HOSTED (no un-host churn), (2) still ONLINE (deliverable), (3) ready\n*235|    // marker still PRESENT. A resident-model teardown (the deleted v0.13.2 shape) would\n 236|    // fail all three — that is the RED-first guard-revert.\n 237|    assert_eq!(set.len(), 1, \"the endpoint is NOT un-hosted (no rehost churn)\");\n 238|    assert_eq!(\n...\n 247|\n*248|    // Teardown: flip offline + reconcile so the stop-side un-hosts + joins the driver\n 249|    // thread (un-hosting a genuinely-offline endpoint is legit — the point is that a\n 250|    // FAILING PSYCHE never triggered it above).\n 251|    spt_store::info::set_status(&perch_path, spt_store::liveness::STATUS_OFFLINE).unwrap();\n## redispatch_stall.rs\n 2|//! Amendment, KNOWN-HAZARDS 7.43, docs/REDISPATCH-STALL-TRIAGE.md): the v0.34\n*3|//! field stalls (20-30s PTY/RC freezes, 17-62s DISPATCH tails) were claim\n 4|//! retries × the broad `Err(_)` opener fallback manufacturing throwaway peek\n 5|//! subscribers whose `StreamLog::attach` replayed the whole ring UNDER the\n 6|//! per-stream mutex against wedged conns — serial 15s bounded-write poison\n...\n 459|    // serves again — one poison never permanently dead-ends the stream.\n*460|    // Subscribe the new generation from the ring TAIL (empty replay): the\n 461|    // recovery contract under test is the LEASE renewal, not the replay (T1\n 462|    // proves that) — and a from-0 subscriber that never drains the 1.5MB\n 463|    // flood would legitimately wedge and re-cancel, testing nothing.\n...\n 468|            assert!(Instant::now() < end, \"the lease never renewed for a new generation\");\n*469|            let tail = probe\n 470|                .net_streams()\n 471|                .expect(\"net-streams\")\n 472|                .streams\n...\n 477|            let mut f = connect_retry(&name_a);\n*478|            f.net_stream_subscribe(sid_a, tail).expect(\"recovery subscribe\");\n 479|            // Give the fire-and-forget subscribe a beat to process, then\n 480|            // probe the lease with a real send on a separate conn.\n 481|            thread::sleep(Duration::from_millis(300));\n## resume.rs\n 17|//!   the floor — the dedup-below + snap-above discipline accepts the floor-jump\n*18|//!   with no gap-error, losing only the evicted tail.\n 19|\n 20|use std::sync::atomic::{AtomicU32, Ordering};\n 21|use std::sync::{Arc, OnceLock};\n...\n 142|    let mut k = connect_retry(name);\n*143|    let _ = k.attach(sid, u64::MAX); // subscribe past the tail (no replay), set session_id\n 144|    let _ = k.kill_session();\n 145|}\n 146|\n...\n 231|// discipline (no reject-gap) accepts the floor-jump with no error, losing only\n*232|// the evicted tail. A strict `next_seq` cursor would gap-reject here and kill the\n 233|// very resume the clamp is meant to save.\n 234|#[test]\n 235|fn resume_below_ring_floor_clamps_forward_without_gap_error() {\n...\n 264|    // Reading must NOT gap-error on the floor-jump (the whole point): we reach\n*265|    // the tail marker, accepting forward from the ring floor.\n 266|    let mut buf = Vec::new();\n 267|    read_session_until(&mut r, sid, b\"SXMARK12\", &mut buf);\n 268|\n*269|    // Evicted tail lost only: SXMARK1 (below cursor, delivered) is absent, and a\n 270|    // chunk evicted before the surviving floor (SXMARK3) is also absent — the\n 271|    // resume jumped the floor rather than replaying it.\n 272|    assert!(\n## shellchan.rs\n 232|\n*233|    // The hung instance is still killable (teardown unblocked).\n 234|    spt_daemon::shellhost::kill_shell_at(&a_perch);\n 235|}\n## two_origin_spanning.rs\n 193|// are a superset replay of gen A (identical role/ts/text at the same localseq)\n*194|// plus B's own new tail. RED-first: WITHOUT the supersede pass every A row surfaces\n 195|// TWICE (once per ordinal) — pre-fix `inputs` would be [a-one, a-two, a-one, a-two,\n*196|// b-tail]. Post-fix: each logical row appears ONCE under B's generation, B's tail\n 197|// intact, and the now-fully-superseded ancestor leaves no orphaned /clear divider.\n 198|#[test]\n 199|fn digest_span_supersedes_a_replayed_generation() {\n...\n 204|    // Gen A: two input rows. Gen B: the SAME two rows replayed verbatim (a\n*205|    // carry-forward checkpoint) at the same localseq, then B's own new tail row.\n 206|    std::fs::write(\n 207|        hp.join(\"sess-A.jsonl\"),\n 208|        \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"a-one\\\",\\\"ts\\\":\\\"2026-06-13T21:00:00Z\\\"}\\n\\\n...\n 215|         {\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"a-two\\\",\\\"ts\\\":\\\"2026-06-13T21:00:01Z\\\"}\\n\\\n*216|         {\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"b-tail\\\",\\\"ts\\\":\\\"2026-06-13T21:00:05Z\\\"}\\n\",\n 217|    )\n 218|    .unwrap();\n 219|\n...\n 265|    // windowing: without supersede the span carries FIVE input turns\n*266|    // (a-one, a-two, [clear], a-one, a-two, b-tail) and all survive the window —\n 267|    // RED. With supersede the ancestor's two rows collapse into B's generation.\n 268|    let over = DigestOverride { window_turns: Some(10), ..Default::default() };\n 269|    let digest = project_endpoint_digest(\"ccgen\", &over);\n...\n 272|        inputs,\n*273|        vec![\"a-one\", \"a-two\", \"b-tail\"],\n*274|        \"each replayed row appears ONCE (under B's gen) + B's tail — no gen-union double\"\n 275|    );\n 276|    // Gen A fully superseded (all rows replayed into B) → its /clear divider is\n 277|    // orphaned and trimmed (rule 5: a divider sits only between ≥1-row sessions).\n\nShowing files 1-20 of 90+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt-daemon/tests, crates/spt-daemon/src, crates/spt/tests, crates/spt/src, crates/spt-term/tests, crates/spt-term/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":88,"fileCount":20,"files":["crates/spt-daemon/tests/brain_decouple.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/dispatch.rs","crates/spt-daemon/tests/driven_by_selfheal.rs","crates/spt-daemon/tests/endpoint_survival.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/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","crates/spt-daemon/tests/pairjoin.rs","crates/spt-daemon/tests/psyche_context_file_e2e.rs","crates/spt-daemon/tests/psyche_residency_expectation_e2e.rs","crates/spt-daemon/tests/redispatch_stall.rs","crates/spt-daemon/tests/resume.rs","crates/spt-daemon/tests/shellchan.rs","crates/spt-daemon/tests/two_origin_spanning.rs"],"fileMatches":[{"path":"crates/spt-daemon/tests/brain_decouple.rs","count":3},{"path":"crates/spt-daemon/tests/broker.rs","count":8},{"path":"crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","count":3},{"path":"crates/spt-daemon/tests/control_stamp_lifetime.rs","count":2},{"path":"crates/spt-daemon/tests/daemon_e2e.rs","count":8},{"path":"crates/spt-daemon/tests/dispatch.rs","count":4},{"path":"crates/spt-daemon/tests/driven_by_selfheal.rs","count":1},{"path":"crates/spt-daemon/tests/endpoint_survival.rs","count":2},{"path":"crates/spt-daemon/tests/false_promote.rs","count":1},{"path":"crates/spt-daemon/tests/idempotent.rs","count":1},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":20},{"path":"crates/spt-daemon/tests/input_ack_deadlock.rs","count":2},{"path":"crates/spt-daemon/tests/legacy_resident_sweep_e2e.rs","count":2},{"path":"crates/spt-daemon/tests/pairjoin.rs","count":2},{"path":"crates/spt-daemon/tests/psyche_context_file_e2e.rs","count":9},{"path":"crates/spt-daemon/tests/psyche_residency_expectation_e2e.rs","count":3},{"path":"crates/spt-daemon/tests/redispatch_stall.rs","count":4},{"path":"crates/spt-daemon/tests/resume.rs","count":5},{"path":"crates/spt-daemon/tests/shellchan.rs","count":1},{"path":"crates/spt-daemon/tests/two_origin_spanning.rs","count":7}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/tests/\n## brain_decouple.rs\n 340│\n*341│    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 342│    if let Some(pid) = broker.session_pid(sid) {\n 343│        kill_pid(pid);\n 344│    }\n    │...\n 411│// deadline; the test captures plain values (bools / Options / Instants) as it\n*412│// goes and only asserts AFTER teardown, so it can never hang even on broken code\n 413│// — worst case it fails an assert after its bounded windows elapse.\n 414│// ─────────────────────────────────────────────────────────────────────────────\n 415│\n    │...\n 754│\n*755│    // ── Teardown BEFORE asserting (existing test's pattern): kill the PTY child and\n 756│    //    stop the broker so a failing assert leaves no orphan child. Drop the\n 757│    //    black-holed Stream only now (the very end). ──\n 758│    if let Some(pid) = broker.session_pid(sid) {\n## broker.rs\n    7│//! kill it and observe the `exit` event. This exercises the whole B1 contract —\n   *8│//! spawn under a broker PTY, stream output, accept input, report exit — through\n    9│//! the actual named-pipe / UDS transport.\n   10│//!\n   11│//! The brain is driven **single-threaded**: send a command, then read frames\n     │...\n   90│    // (No per-read timeout: Windows named pipes reject I/O timeouts. The broker\n  *91│    // always pushes `spawned`/`output`/`exit` frames, so the blocking reads\n   92│    // below return; a genuine stall is caught by the test runner's own timeout.)\n   93│\n   94│    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n     │...\n  161│    // command loop hits EOF and tears the session down. Joining the handler\n *162│    // (via the server thread) lets us assert the teardown happened.\n  163│    drop(conn);\n  164│    server.join().expect(\"server thread\");\n  165│\n     │...\n  376│    // $SPT_ENDPOINT_ID` → exits) prints once then dies, so on a fast/quiet box the\n *377│    // EXIT notification and a transient read Err can both arrive BEFORE the OUTPUT\n  378│    // frame is delivered. The old loop broke on the first `Err` OR on `EXIT`, so it\n  379│    // bailed with out=\"\" (a rotating contention victim, then an 18ms fast-fail on\n  380│    // the serial Phase-B box). Instead RETRY on Err (every other read loop in this\n  381│    // file does `Err => sleep(10ms)`), bound by a wall-clock deadline, and — once the\n *382│    // child has EXITed — keep draining briefly so a trailing OUTPUT frame still lands\n  383│    // before we give up.\n  384│    let mut out = Vec::<u8>::new();\n  385│    let mut got = false;\n     │...\n  399│                // After EXIT a run of read errors means the stream is fully drained\n *400│                // (no trailing output is coming) — stop. Before EXIT the frame just\n  401│                // isn't here yet, so wait and retry.\n  402│                if exited {\n  403│                    post_exit_drains += 1;\n     │...\n 1172│    // Byte-exactness: the consumer's ACCEPTED stream (live prefix + post-subscribe\n*1173│    // tail) is exactly-once — every marker appears, none duplicated into the\n 1174│    // accepted record by the replay (dedup held). The full accepted seq vector is\n 1175│    // strictly increasing (re-affirms monotonic, independent of the in-loop assert).\n 1176│    assert!(\n     │...\n 1191│\n*1192│    // Clean teardown: kill the child, drain to the exit, drop the conn.\n 1193│    send(\n 1194│        &mut conn,\n 1195│        KIND_KILL,\n## conn_blackhole_lifecycle.rs\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  55│//! VIEWER attach (not just a controller take) proven to receive output.\n    │...\n  76│//! this test rides a bounded thread+channel or a polled deadline; the test\n *77│//! captures plain values as it goes and only asserts AFTER teardown, so it can\n  78│//! never hang even on broken code.\n  79│#![cfg(windows)]\n  80│\n    │...\n 582│\n*583│    // ── Teardown BEFORE asserting (so a failing assert leaves no orphan child):\n 584│    //    kill both PTY children, stop the broker, drop the black-holed Stream. ──\n 585│    if let Some(pid) = broker.session_pid(sid_b) {\n 586│        kill_pid(pid);\n## control_stamp_lifetime.rs\n 10│//! `/exit` ends the RC session INDIRECTLY: the endpoint suspends when the child\n*11│//! binary terminates, so the teardown runs the REAP path (exit-waiter →\n 12│//! `sessions.remove`), NOT the controller-detach path (`detach_if` →\n 13│//! `clear_controller` → `stamp_driven_by`, the ONLY pre-fix stamp-clear). The reap\n 14│//! path dropped the OutputLog with the controller slot still occupied, so the\n   │...\n 20│//!\n*21│//! The exit-waiter calls `OutputLog::stamp_reaped()` on reap, which clears all\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## daemon_e2e.rs\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  24│//!\n  25│//! The child is the same cross-OS echo program the other daemon tests use (`cat` /\n    │...\n  89│// A stdin→stdout passthrough so the echo-commune summarizer writes a real drop\n *90│// file — lets the signoff assert the echo (3.3) actually ran before teardown.\n  91│#[cfg(windows)]\n  92│const PASSTHROUGH: &str = \"sort\";\n  93│#[cfg(unix)]\n    │...\n 286│\n*287│    // ── 5. graceful-signoff: echo BEFORE teardown (3.3), ready marker removed ───\n*288│    // Seed the Self's ready marker (graceful teardown removes it).\n 289│    let ready = perch::resolve_ready_file(id, ParentHint::Infer);\n 290│    std::fs::create_dir_all(ready.parent().unwrap()).unwrap();\n 291│    std::fs::write(&ready, \"\").unwrap();\n    │...\n 299│        drops.path().join(\"doyle-commune.md\").exists(),\n*300│        \"the echo wrote a final-delta commune before teardown\"\n 301│    );\n*302│    assert!(!ready.exists(), \"teardown removed the Self's ready marker\");\n 303│\n*304│    // The hosted Psyche is marked offline on teardown (the online-stamp\n 305│    // counterpart). mark_offline is a definitive-death observation, so it also\n 306│    // terminal-normalizes the rest intent (ADR-0041 decision 3).\n 307│    // [unit->REQ-REST-TERMINAL-NORMALIZE]\n## dispatch.rs\n 915│    .expect(\"relink round trip\");\n*916│    let ShellLinkRequestOutcome::Ok(detail) = &out else {\n 917│        panic!(\"{out:?}\")\n 918│    };\n*919│    assert!(detail.contains(&format!(\"relinked {id}\")), \"{detail}\");\n 920│    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, \"doyle-d8c\", &id);\n 921│    let token =\n 922│        spt_daemon::shellhost::read_link_token(&shell_perch).expect(\"fresh token parked at B\");\n    │...\n 936│    .expect(\"cmd round trip\");\n*937│    let ShellLinkRequestOutcome::Ok(detail) = &out else {\n 938│        panic!(\"{out:?}\")\n 939│    };\n*940│    assert!(detail.contains(\"spooled op=note\"), \"{detail}\");\n 941│    let rows = spool::peek_all_at(&shell_perch).expect(\"spool\");\n 942│    assert_eq!(rows.len(), 1, \"exactly one frame spooled at B\");\n 943│    let key = spt_daemon::shellhost::link_key(&token);\n## driven_by_selfheal.rs\n 385│\n*386│    // Teardown: reap the PTY child (scoped) so the live session never leaks.\n 387│    if let Some(pid) = broker.session_pid(sid) {\n 388│        kill_pid(pid);\n 389│    }\n## endpoint_survival.rs\n 134│\n*135│    // Best-effort teardown: kill the long-lived harness child before asserting.\n 136│    if let Some(sid) = survivor_session_id(&name) {\n 137│        if let Some(pid) = broker.session_pid(sid) {\n 138│            kill_pid(pid);\n    │...\n 160│\n*161│/// The broker session id hosting the \"survivor\" endpoint, if any (for teardown).\n 162│fn survivor_session_id(name: &str) -> Option<u64> {\n 163│    let mut brain = Brain::cold_start(name, now_ms()).ok()?;\n 164│    brain\n## false_promote.rs\n 350│\n*351│    // Teardown BEFORE asserting (so a failing assert still leaves no flood child).\n 352│    stop.store(true, Ordering::Relaxed);\n 353│    if let Some(pid) = broker.session_pid(sid) {\n 354│        kill_pid(pid);\n## idempotent.rs\n 181│        Some((6, Window::BeforeEffect)),\n*182│        None, // a clean life delivers the tail\n 183│    ];\n 184│\n 185│    let mut state = brain.snapshot();\n## inject_control_wedge.rs\n  147│\n *148│/// Join a teardown helper thread, but NEVER ride a permanently-parked helper to\n  149│/// the nextest slow-timeout backstop (240s). On the FAIL/wedge path a helper can\n  150│/// be parked forever inside a blocking socket op — e.g. the driver's `send_effect`\n  151│/// on a wedged broker never returns to observe `stop` — so a bare `.join()` would\n     │...\n  153│/// gate result (`subscribed`/`got_output`) is already captured by the 30s watchdog\n *154│/// BEFORE teardown, so bounding the join lets a real product wedge fail FAST on the\n  155│/// assertion (~watchdog time) instead of timing out. A timed-out helper is\n  156│/// abandoned — it dies at process exit, exactly like the KH-7.6 abandoned\n  157│/// pump-ipc-reader threads this file already relies on. Returns true iff it joined.\n     │...\n  167│/// Read `Broker::session_pid` under a bound. The pid is only used to best-effort\n *168│/// kill the flood child during teardown, but `session_pid` locks the broker's\n  169│/// `sessions` Mutex — if a wedged per-conn handler is holding that lock the read\n *170│/// blocks and rides teardown to the 240s backstop. Probe it on a detached thread\n  171│/// (the broker is an `Arc`, so the clone keeps it alive) and give up on timeout;\n  172│/// a missed child-kill is harmless, the process-per-test exit reaps it.\n  173│fn session_pid_bounded(broker: &Arc<Broker>, sid: u64, dur: Duration) -> Option<u32> {\n     │...\n  210│/// A QUIET, long-lived child for the w5_a2 IDLE characterization (doyle gate\n *211│/// ruling, Option B): it produces NO output and does NOT exit, so the session is\n  212│/// GENUINELY idle for the read window. (`flood_spawn_req` floods through the read,\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     │...\n  650│        // Sustain journaled input: keep ops in flight (and, on Unix, parked under\n *651│        // the journal lock) until teardown. Each send_effect writes a frame; the\n  652│        // pump reader drains replies concurrently so the write never deadlocks.\n  653│        while !driver_stop.load(std::sync::atomic::Ordering::Relaxed) {\n  654│            if w.send_effect(MintedOp::new(Minter::Rc, op), &chunk).is_err() {\n     │...\n  811│    // Watchdog: a hard wedge surfaces as a recv timeout (a test FAILURE), never a\n *812│    // hang. The attacher reports its result before any blocking teardown, so this\n  813│    // recv is the hard ceiling on the whole gate. Deadline is env-tunable\n  814│    // (attach_gate_watchdog: default 30s tight, CI-generous): a real wedge blocks\n  815│    // FOREVER so it trips any finite deadline — the knob only grants headroom for a\n     │...\n  843│    stop.store(true, std::sync::atomic::Ordering::Relaxed);\n *844│    // Bound the teardown joins: on a REAL facet-B wedge the driver stays parked in\n  845│    // `send_effect` and the attacher in its subscribe — neither observes `stop`, so\n  846│    // a bare `.join()` would ride to the 240s nextest backstop and MASK the verdict\n  847│    // (the symptom doyle's kitsubito reliability run hit). The gate result is already\n     │...\n  859│\n *860│    // ── PROCESS-CLEAN-EXIT teardown (test-robustness, not the gate). ──\n  861│    // Each `cold_start_pump` Brain (the driver's `w`, the attacher's `operator`)\n  862│    // spawns a DETACHED `pump-ipc-reader` thread parked in a BLOCKING named-pipe\n  863│    // `read_frame` on its RecvHalf. Joining the helper threads drops those Brains,\n     │...\n  868│    // On Windows, if the test returns before that chain completes, a reader thread\n *869│    // is still parked inside a named-pipe read syscall at process teardown, and a\n  870│    // thread blocked in that syscall on a half-closed handle aborts the WHOLE\n  871│    // process with exit 0xffffffff (the flaky ~50% abort under `--test-threads=1`,\n  872│    // where the next test / process exit races the chain). FIX: after the Brains\n     │...\n  875│    // moves on. Verified by `PUMP_IPC_READER: exited` printing within this window.\n *876│    // This is purely teardown hygiene; the assertions below are unchanged.\n  877│    thread::sleep(Duration::from_millis(750));\n  878│\n  879│    eprintln!(\n     │...\n 1301│        }\n*1302│        controller // hand it back so it is dropped only after teardown\n 1303│    });\n 1304│\n 1305│    // ── Deliver an inbound message as a real `<EVENT…>` envelope via KIND_ENDPOINT_INPUT.\n     │...\n 1331│\n*1332│    // Teardown: reap the PTY child + the controller drainer.\n 1333│    if let Some(pid) = broker.session_pid(sid) {\n 1334│        kill_pid(pid);\n 1335│    }\n     │...\n 1603│    // it: the `xlate_choreo_fixture` process is a child of THIS test process. We\n*1604│    // confirm it is live before teardown, then assert it is gone after.\n 1605│    let xlate_pid = find_xlate_child_pid().expect(\"translation child should be running\");\n 1606│    assert!(\n 1607│        pid_alive(xlate_pid),\n*1608│        \"the translation child must be live before teardown (sanity)\"\n 1609│    );\n 1610│\n 1611│    // Tear the session down: kill the PTY child → the exit waiter reaps the session\n     │...\n 1664│        \"doyle gate #2 (no-zombie): the translation child must be terminated + reaped \\\n*1665│         within the bounded reap window after session-down / broker teardown — a \\\n 1666│         session-down must never leak a translation child\"\n 1667│    );\n 1668│}\n     │...\n 2307│    // serve_attach / become_controller) with a QUIET long-lived child\n*2308│    // (`quiet_spawn_req`, no output, never exits, killed only at teardown — no mid-read\n 2309│    // kill, no Windows reap-during-read flake).\n 2310│    init_wedge_home();\n 2311│    let dir = tempfile::tempdir().expect(\"tempdir\");\n     │...\n 2334│    // read with NO mid-read kill — which also removes the Windows reap-during-read\n*2335│    // flake the old flood-then-kill rig hit. The child is killed only at teardown.\n 2336│    let mut spawner = connect_retry(&name);\n 2337│    let sid = spawner\n 2338│        .spawn_session(quiet_spawn_req(endpoint))\n     │...\n 2390│\n*2391│    // Capture the child pid for TEARDOWN only — the quiet child is NEVER killed\n 2392│    // mid-read (it stays live + silent through the read window, so the idle slot is\n 2393│    // faithfully observable and the broker keeps the session row).\n 2394│    let child_pid = _broker.session_pid(sid);\n     │...\n 2401│\n*2402│    // Teardown.\n 2403│    stop.store(true, Ordering::Relaxed);\n 2404│    if let Some(pid) = child_pid {\n 2405│        kill_pid(pid);\n## input_ack_deadlock.rs\n 514│\n*515│    // ── Teardown: both verdicts are already captured off the watchdog'd channels,\n 516│    //    so the GATE never depends on a helper thread terminating. Kill the echo\n 517│    //    child (closes the PTY → unwinds the abandoned serve_attach threads + their\n 518│    //    target conns), then DETACH the helper handles (do NOT join — a serve_attach\n    │...\n 527│    // Settle so the child-kill close chain reaches the abandoned serve threads before\n*528│    // the process exits (teardown hygiene; the gate is already decided above).\n 529│    thread::sleep(Duration::from_millis(750));\n 530│\n 531│    eprintln!(\n## legacy_resident_sweep_e2e.rs\n  49│}\n *50│// A trailing shell builtin (`:`) keeps the shell RESIDENT (no tail-exec-replace by\n  51│// `sleep`) so /proc/<pid>/cmdline retains the full `-c` string incl the marker.\n  52│#[cfg(unix)]\n  53│fn spawn_marked(marker: &str) -> std::process::Child {\n    │...\n 183│\n*184│    // Teardown: tree-reap the control (its shell + the long-lived ping/sleep grandchild)\n 185│    // by pid — the mock tree was already reaped by the sweep. Own spawned pid tree only,\n 186│    // never a broad reap.\n 187│    spt_store::proc::kill_pid_tree(control_pid);\n## pairjoin.rs\n 100│    assert!(\n*101│        reply.detail.contains(\"expired\") || reply.detail.contains(\"re-run\"),\n 102│        \"the failure guides the operator to re-run the join, got: {}\",\n*103│        reply.detail\n 104│    );\n 105│}\n 106│\n## psyche_context_file_e2e.rs\n  14│//! non-empty stdout. The test asserts the proof holds the WHOLE ~40KB mind (both a\n *15│//! head AND a tail marker present, length >= the seed) — proving the mind reached the\n  16│//! role via the FILE, unbroken/untruncated, and never rode argv.\n  17│//!\n  18│//! RED-FIRST (guard-revert, documented): in\n    │...\n  38│/// The ~40KB preload the FreshWithPreload turn composes (a real doyle-scale mind).\n *39│/// HEAD + TAIL markers bracket a 40KB filler so a TRUNCATED delivery (the argv-cap\n *40│/// brick) loses the tail — a full-file delivery keeps both.\n  41│const ROLE_HEAD: &str = \"ROLE-HEAD-MARKER-7f3a\";\n *42│const ROLE_TAIL: &str = \"ROLE-TAIL-MARKER-9b2c\";\n  43│fn big_role() -> String {\n *44│    format!(\"{ROLE_HEAD}\\n{}\\n{ROLE_TAIL}\", \"x\".repeat(40_000))\n  45│}\n  46│\n  47│#[cfg(windows)]\n    │...\n 182│    let on_disk = std::fs::read_to_string(&context_file).unwrap();\n*183│    assert!(on_disk.contains(ROLE_HEAD) && on_disk.contains(ROLE_TAIL));\n 184│\n 185│    // THE LOAD-BEARING CLAIM: the FULL ~40KB mind reached the role VIA THE FILE — both\n*186│    // the head AND the tail survived (a truncated argv delivery would drop the tail),\n 187│    // and the proof (the role's own copy of {psyche_context_file}) is >= the seed size.\n 188│    // [int->REQ-PSYCHE-CONTEXT-FILE-INDIRECTION]\n 189│    assert!(\n    │...\n 193│    assert!(\n*194│        proof_body.contains(ROLE_TAIL),\n*195│        \"the role read the mind's TAIL from the file — the full body arrived UNTRUNCATED \\\n 196│         (a mind-on-argv path would brick/truncate at the win32 32k cap)\"\n 197│    );\n 198│    assert!(\n## psyche_residency_expectation_e2e.rs\n  36│//! RED-FIRST (guard-revert, documented): re-introduce a destructive `set.stop_host(id)`\n *37│//! / OFFLINE-stamp on the turn-failure arm (the exact v0.13.2-shape teardown W3\n  38│//! deleted) → the endpoint un-hosts, its status flips offline, and its ready marker is\n  39│//! removed → every \"stays deliverable / stays hosted\" assertion below goes RED.\n  40│//!\n    │...\n 234│    // (1) still HOSTED (no un-host churn), (2) still ONLINE (deliverable), (3) ready\n*235│    // marker still PRESENT. A resident-model teardown (the deleted v0.13.2 shape) would\n 236│    // fail all three — that is the RED-first guard-revert.\n 237│    assert_eq!(set.len(), 1, \"the endpoint is NOT un-hosted (no rehost churn)\");\n 238│    assert_eq!(\n    │...\n 247│\n*248│    // Teardown: flip offline + reconcile so the stop-side un-hosts + joins the driver\n 249│    // thread (un-hosting a genuinely-offline endpoint is legit — the point is that a\n 250│    // FAILING PSYCHE never triggered it above).\n 251│    spt_store::info::set_status(&perch_path, spt_store::liveness::STATUS_OFFLINE).unwrap();\n## redispatch_stall.rs\n   2│//! Amendment, KNOWN-HAZARDS 7.43, docs/REDISPATCH-STALL-TRIAGE.md): the v0.34\n  *3│//! field stalls (20-30s PTY/RC freezes, 17-62s DISPATCH tails) were claim\n   4│//! retries × the broad `Err(_)` opener fallback manufacturing throwaway peek\n   5│//! subscribers whose `StreamLog::attach` replayed the whole ring UNDER the\n   6│//! per-stream mutex against wedged conns — serial 15s bounded-write poison\n    │...\n 459│    // serves again — one poison never permanently dead-ends the stream.\n*460│    // Subscribe the new generation from the ring TAIL (empty replay): the\n 461│    // recovery contract under test is the LEASE renewal, not the replay (T1\n 462│    // proves that) — and a from-0 subscriber that never drains the 1.5MB\n 463│    // flood would legitimately wedge and re-cancel, testing nothing.\n    │...\n 468│            assert!(Instant::now() < end, \"the lease never renewed for a new generation\");\n*469│            let tail = probe\n 470│                .net_streams()\n 471│                .expect(\"net-streams\")\n 472│                .streams\n    │...\n 477│            let mut f = connect_retry(&name_a);\n*478│            f.net_stream_subscribe(sid_a, tail).expect(\"recovery subscribe\");\n 479│            // Give the fire-and-forget subscribe a beat to process, then\n 480│            // probe the lease with a real send on a separate conn.\n 481│            thread::sleep(Duration::from_millis(300));\n## resume.rs\n  17│//!   the floor — the dedup-below + snap-above discipline accepts the floor-jump\n *18│//!   with no gap-error, losing only the evicted tail.\n  19│\n  20│use std::sync::atomic::{AtomicU32, Ordering};\n  21│use std::sync::{Arc, OnceLock};\n    │...\n 142│    let mut k = connect_retry(name);\n*143│    let _ = k.attach(sid, u64::MAX); // subscribe past the tail (no replay), set session_id\n 144│    let _ = k.kill_session();\n 145│}\n 146│\n    │...\n 231│// discipline (no reject-gap) accepts the floor-jump with no error, losing only\n*232│// the evicted tail. A strict `next_seq` cursor would gap-reject here and kill the\n 233│// very resume the clamp is meant to save.\n 234│#[test]\n 235│fn resume_below_ring_floor_clamps_forward_without_gap_error() {\n    │...\n 264│    // Reading must NOT gap-error on the floor-jump (the whole point): we reach\n*265│    // the tail marker, accepting forward from the ring floor.\n 266│    let mut buf = Vec::new();\n 267│    read_session_until(&mut r, sid, b\"SXMARK12\", &mut buf);\n 268│\n*269│    // Evicted tail lost only: SXMARK1 (below cursor, delivered) is absent, and a\n 270│    // chunk evicted before the surviving floor (SXMARK3) is also absent — the\n 271│    // resume jumped the floor rather than replaying it.\n 272│    assert!(\n## shellchan.rs\n 232│\n*233│    // The hung instance is still killable (teardown unblocked).\n 234│    spt_daemon::shellhost::kill_shell_at(&a_perch);\n 235│}\n## two_origin_spanning.rs\n 193│// are a superset replay of gen A (identical role/ts/text at the same localseq)\n*194│// plus B's own new tail. RED-first: WITHOUT the supersede pass every A row surfaces\n 195│// TWICE (once per ordinal) — pre-fix `inputs` would be [a-one, a-two, a-one, a-two,\n*196│// b-tail]. Post-fix: each logical row appears ONCE under B's generation, B's tail\n 197│// intact, and the now-fully-superseded ancestor leaves no orphaned /clear divider.\n 198│#[test]\n 199│fn digest_span_supersedes_a_replayed_generation() {\n    │...\n 204│    // Gen A: two input rows. Gen B: the SAME two rows replayed verbatim (a\n*205│    // carry-forward checkpoint) at the same localseq, then B's own new tail row.\n 206│    std::fs::write(\n 207│        hp.join(\"sess-A.jsonl\"),\n 208│        \"{\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"a-one\\\",\\\"ts\\\":\\\"2026-06-13T21:00:00Z\\\"}\\n\\\n    │...\n 215│         {\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"a-two\\\",\\\"ts\\\":\\\"2026-06-13T21:00:01Z\\\"}\\n\\\n*216│         {\\\"role\\\":\\\"input\\\",\\\"text\\\":\\\"b-tail\\\",\\\"ts\\\":\\\"2026-06-13T21:00:05Z\\\"}\\n\",\n 217│    )\n 218│    .unwrap();\n 219│\n    │...\n 265│    // windowing: without supersede the span carries FIVE input turns\n*266│    // (a-one, a-two, [clear], a-one, a-two, b-tail) and all survive the window —\n 267│    // RED. With supersede the ancestor's two rows collapse into B's generation.\n 268│    let over = DigestOverride { window_turns: Some(10), ..Default::default() };\n 269│    let digest = project_endpoint_digest(\"ccgen\", &over);\n    │...\n 272│        inputs,\n*273│        vec![\"a-one\", \"a-two\", \"b-tail\"],\n*274│        \"each replayed row appears ONCE (under B's gen) + B's tail — no gen-union double\"\n 275│    );\n 276│    // Gen A fully superseded (all rows replayed into B) → its /clear divider is\n 277│    // orphaned and trimmed (rule 5: a divider sits only between ≥1-row sessions)."},"isError":false,"timestamp":1784365558671}}
{"type":"custom_message","customType":"irc:incoming","content":"<irc>\nIncoming IRC message from agent `PickerWhitespaceRca`:\n\nPicker is independent: crates/spt/src/picker/mod.rs setup_terminal/event_loop uses ratatui Terminal<CrosstermBackend<Stdout>> direct to local stdout; no spt-term::ScreenGrid or rc pump until picker exits/restores alt screen. High-confidence purge root: event_loop lines 165-173 calls cli::cmd_endpoint_purge inline while alt-screen/diff model live; cli.rs cmd_endpoint_purge lines 12707-12826 emits eprintln! incl PURGED at 12825. That out-of-band stderr mutates same terminal screen/cursor, but ratatui’s internal previous Buffer is unaware, so next diff skips unchanged cells/spaces and leaves glyphs. Deterministic seam: stateful TestBackend or recording Backend: draw ConfirmPurge, inject an external glyph into backend/display without changing Terminal prev buffer, transition PickExisting/remove row, draw; assert full visible screen. Better direct regression seam factor inline outcome handler with injected purge sink and call terminal.clear()/swap/reset before next draw; existing view TestBackend only tests isolated complete buffers, can’t catch diff desync.\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: \"PickerWhitespaceRca\"`) — you may finish your current step first. Nobody replies on your behalf.\n</irc>","display":true,"details":{"id":"15346bbbeb9c5f81","from":"PickerWhitespaceRca","message":"Picker is independent: crates/spt/src/picker/mod.rs setup_terminal/event_loop uses ratatui Terminal<CrosstermBackend<Stdout>> direct to local stdout; no spt-term::ScreenGrid or rc pump until picker exits/restores alt screen. High-confidence purge root: event_loop lines 165-173 calls cli::cmd_endpoint_purge inline while alt-screen/diff model live; cli.rs cmd_endpoint_purge lines 12707-12826 emits eprintln! incl PURGED at 12825. That out-of-band stderr mutates same terminal screen/cursor, but ratatui’s internal previous Buffer is unaware, so next diff skips unchanged cells/spaces and leaves glyphs. Deterministic seam: stateful TestBackend or recording Backend: draw ConfirmPurge, inject an external glyph into backend/display without changing Terminal prev buffer, transition PickExisting/remove row, draw; assert full visible screen. Better direct regression seam factor inline outcome handler with injected purge sink and call terminal.clear()/swap/reset before next draw; existing view TestBackend only tests isolated complete buffers, can’t catch diff desync."},"attribution":"agent","id":"1e5af0b1","parentId":"0b2c1502","timestamp":"2026-07-18T09:05:58.696Z"}
{"type":"message","id":"42631dc9","parentId":"1e5af0b1","timestamp":"2026-07-18T09:06:08.098Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Verifying exact byte-level reproduction**"},{"type":"toolCall","id":"call_v1WOGGgXnpMo0veeCXioihIV|fc_0ce5441d29b9f05d016a5b41fff7b0819ba67ab81a369dc969","name":"read","arguments":{"i":"Reading admitted exit race test","path":"crates/spt-daemon/tests/broker.rs","selector":"340-425"}},{"type":"toolCall","id":"call_twjv2OxobSGGoe3D3ytQZNnn|fc_0ce5441d29b9f05d016a5b41fff7c0819bb0cc53a1d3434ade","name":"read","arguments":{"i":"Reading network attach protocol","path":"crates/spt-net/src/net/attach.rs","selector":"1-220"}},{"type":"toolCall","id":"call_wfilJHw3y4NNXljmo9EZnyK7|fc_0ce5441d29b9f05d016a5b41fff7c8819b990b62358628928c","name":"read","arguments":{"i":"Reading output event codec","path":"crates/spt-daemon/src/msg.rs","selector":"540-570,1160-1190"}},{"type":"toolCall","id":"call_qiEUi6DAkvjhHUOAycPFvm8n|fc_0ce5441d29b9f05d016a5b41fff7d0819b8cc971911fbbd8c7","name":"read","arguments":{"i":"Reading brain output cursor path","path":"crates/spt-daemon/src/brain.rs","selector":"680-760,960-1010"}},{"type":"toolCall","id":"call_KhJK4tl1gZs7Wrdg3YwCFetr|fc_0ce5441d29b9f05d016a5b41fff7d4819b98b422930089324b","name":"read","arguments":{"i":"Reading connection timeout implementation","path":"crates/spt-daemon/src/conn.rs","selector":"410-620"}},{"type":"toolCall","id":"call_XsRamIatGYKdiFjvO6Mamr2n|fc_0ce5441d29b9f05d016a5b41fff7dc819bb364c12f4c165263","name":"read","arguments":{"i":"Reading picker interference seam","path":"crates/spt/src/picker/mod.rs","selector":"130-200"}},{"type":"toolCall","id":"call_ZjS5njP2FOoNqsGHhe1M2Qxc|fc_0ce5441d29b9f05d016a5b41fff7e0819bb93b712687a5dc9e","name":"read","arguments":{"i":"Reading purge stderr output","path":"crates/spt/src/cli.rs","selector":"12700-12835"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":18555,"output":331,"cacheRead":178688,"cacheWrite":0,"totalTokens":197574,"reasoningTokens":53,"cost":{"input":0.09277500000000001,"output":0.00993,"cacheRead":0.08934399999999999,"cacheWrite":0,"total":0.192049}},"stopReason":"toolUse","timestamp":1784365558714,"responseId":"resp_0ce5441d29b9f05d016a5b41f70b1c819b9b79b769f04cef1a","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b41f9d818819ba02a6125837968a5","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0H_Fbad_YMUQc2vA6wI7GiC1ZKWFFKYO5UWA958tnW6Dpx9c0EZlKKPoqTqbhsHtwwDz5yyubl2wFDNoHfJhbZbdarMqyOHIz0tjSlTbKU87CjK11a9xT3G1II27AK6m35LU6Zmx7ToUNtlJTuLE5GJuxVIcMrio8Tp26DvtkRc7XoyUuHh_C5OlwT4mhFzEQQwRhc4IZovzFfF0h9Prr_FWNezi264Y3cBNSLqMNdu_VqyIwbHU7xEl3KmbHMDbuvuWPpbngEFDp4-LCIkCjup80b2RmitFeVPQU7nSCTKlnzcD8qyBjWAKIdcUJyajhokCNDGC2pP3XjYTsVlQAhWq7kGMKlfS4lpnfIfncvCGsfzGZdT5fqmlxQIsoesmk0hMRQ7KcMeL1y7aApAyKUdIdDkNB9GrayOpIO5jRIq4bX6iYPCMAuiwYHGT3___Y66_Eoj7sT2ZoY0ORR4gaQj_OPYqpv_V34Dx_xLZCMBBEFjVEvBZuSTeIVn9sGEr2zbtwDNR58656dqKmXa5ClDUjyNFvii7zoDTaov_IkWppDha2WSdYUbATWmhoh2FHrelDSGFz8VJWxDH9WvYmCeYHUSNIWR12WUaB2a9y1Gb9_cnhc50t6mXp9_uaSr5otgUXmus2Rj4rIavgxAL_ukU6-Hah7XGoOcvcDhgzQu87EWSHVmVIuEdCVCxCDCvivI5P7admIlCTKIB-PRNBL9z4R7DRYL9v7nJCQ_q52ZdVO1E40lPr4AhDTGdWCGmPSJW6noPrlryEI0mFUoWti-A5enBTqXhVn3i2-i528gqYZ-vCfZucPDXqbWdT34iwECRoz1r2NIntm0ZtXmYUQXz1bBzimdsmxPDu_P8LGtmfbqOI1ECfdgkCLOk9HgPRhOu3ssSIxdRBPfHfoaurmLbc7ruYycqE1MhSwjelONSUf3KUJiJ1bA63uMpm8JPVv5Ujo8mM7sLNR5yCvyPyehove9rDANwqpZ2O2DJDvNHR5d2PDGZWtAtor3j5diXGJZTMBxgURpmd9pmc1oxxSh9QDr1EybhnxYQZmDJ8pEL3k-9EeEpw3kJ23wZS0lOfD190cAtfBtLO24NTBVAFQYcJORMgbwf94oZlQAM_epkV7S6ifbiHI0_CSFbQ0etVafuEN7XyVHWsp8qvJqtfxHWKQ1N_DmNX2YAsTjft1uf36GQpnH_pKaAHisQfkUgc3E59-6B2Q-oYbuYCwKv00sSXHm-DSRsCMSRKYQ0o9bPZUx9qlxdla6KzCy6bXchY-W","summary":[{"type":"summary_text","text":"**Verifying exact byte-level reproduction**"}]},{"id":"fc_0ce5441d29b9f05d016a5b41fff7b0819ba67ab81a369dc969","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading admitted exit race test\",\"path\":\"crates/spt-daemon/tests/broker.rs\",\"selector\":\"340-425\"}","call_id":"call_v1WOGGgXnpMo0veeCXioihIV","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41fff7c0819bb0cc53a1d3434ade","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading network attach protocol\",\"path\":\"crates/spt-net/src/net/attach.rs\",\"selector\":\"1-220\"}","call_id":"call_twjv2OxobSGGoe3D3ytQZNnn","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41fff7c8819b990b62358628928c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading output event codec\",\"path\":\"crates/spt-daemon/src/msg.rs\",\"selector\":\"540-570,1160-1190\"}","call_id":"call_wfilJHw3y4NNXljmo9EZnyK7","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41fff7d0819b8cc971911fbbd8c7","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading brain output cursor path\",\"path\":\"crates/spt-daemon/src/brain.rs\",\"selector\":\"680-760,960-1010\"}","call_id":"call_qiEUi6DAkvjhHUOAycPFvm8n","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41fff7d4819b98b422930089324b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading connection timeout implementation\",\"path\":\"crates/spt-daemon/src/conn.rs\",\"selector\":\"410-620\"}","call_id":"call_KhJK4tl1gZs7Wrdg3YwCFetr","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41fff7dc819bb364c12f4c165263","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading picker interference seam\",\"path\":\"crates/spt/src/picker/mod.rs\",\"selector\":\"130-200\"}","call_id":"call_XsRamIatGYKdiFjvO6Mamr2n","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b41fff7e0819bb93b712687a5dc9e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading purge stderr output\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"12700-12835\"}","call_id":"call_ZjS5njP2FOoNqsGHhe1M2Qxc","name":"read"}]},"duration":9373.252099990845,"ttft":3146.706499993801,"contextSnapshot":{"promptTokens":197243,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_v1WOGGgXnpMo0veeCXioihIV|fc_0ce5441d29b9f05d016a5b41fff7b0819ba67ab81a369dc969","toolName":"read","startedAt":"2026-07-18T09:06:08.099Z","args":{"path":"crates/spt-daemon/tests/broker.rs"},"intent":"Reading admitted exit race test"},"id":"758ea2fb","parentId":"42631dc9","timestamp":"2026-07-18T09:06:08.099Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_twjv2OxobSGGoe3D3ytQZNnn|fc_0ce5441d29b9f05d016a5b41fff7c0819bb0cc53a1d3434ade","toolName":"read","startedAt":"2026-07-18T09:06:08.099Z","args":{"path":"crates/spt-net/src/net/attach.rs"},"intent":"Reading network attach protocol"},"id":"4b6d1375","parentId":"758ea2fb","timestamp":"2026-07-18T09:06:08.099Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_wfilJHw3y4NNXljmo9EZnyK7|fc_0ce5441d29b9f05d016a5b41fff7c8819b990b62358628928c","toolName":"read","startedAt":"2026-07-18T09:06:08.099Z","args":{"path":"crates/spt-daemon/src/msg.rs"},"intent":"Reading output event codec"},"id":"7c654204","parentId":"4b6d1375","timestamp":"2026-07-18T09:06:08.099Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qiEUi6DAkvjhHUOAycPFvm8n|fc_0ce5441d29b9f05d016a5b41fff7d0819b8cc971911fbbd8c7","toolName":"read","startedAt":"2026-07-18T09:06:08.099Z","args":{"path":"crates/spt-daemon/src/brain.rs"},"intent":"Reading brain output cursor path"},"id":"0f262637","parentId":"7c654204","timestamp":"2026-07-18T09:06:08.099Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KhJK4tl1gZs7Wrdg3YwCFetr|fc_0ce5441d29b9f05d016a5b41fff7d4819b98b422930089324b","toolName":"read","startedAt":"2026-07-18T09:06:08.099Z","args":{"path":"crates/spt-daemon/src/conn.rs"},"intent":"Reading connection timeout implementation"},"id":"ad458e35","parentId":"0f262637","timestamp":"2026-07-18T09:06:08.099Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_XsRamIatGYKdiFjvO6Mamr2n|fc_0ce5441d29b9f05d016a5b41fff7dc819bb364c12f4c165263","toolName":"read","startedAt":"2026-07-18T09:06:08.099Z","args":{"path":"crates/spt/src/picker/mod.rs"},"intent":"Reading picker interference seam"},"id":"76c42983","parentId":"ad458e35","timestamp":"2026-07-18T09:06:08.099Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_ZjS5njP2FOoNqsGHhe1M2Qxc|fc_0ce5441d29b9f05d016a5b41fff7e0819bb93b712687a5dc9e","toolName":"read","startedAt":"2026-07-18T09:06:08.099Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading purge stderr output"},"id":"dc631d12","parentId":"76c42983","timestamp":"2026-07-18T09:06:08.099Z"}
{"type":"message","id":"be617e2e","parentId":"dc631d12","timestamp":"2026-07-18T09:06:08.115Z","message":{"role":"toolResult","toolCallId":"call_qiEUi6DAkvjhHUOAycPFvm8n|fc_0ce5441d29b9f05d016a5b41fff7d0819b8cc971911fbbd8c7","toolName":"read","content":[{"type":"text","text":"    /// 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_SPAWN_CONFLICT => {\n                    let ev: SpawnConflict = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::SpawnConflict {\n                        endpoint: ev.endpoint,\n…\n                    });\n                }\n…\n            }\n        }\n    }\n…\n    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.next_seq = from_seq;\n        // #6 (ADR-0031): a COLD attach receives a clean-screen repaint at pseudo-seq\n        // `broker.next_seq - 1`, not seq 0 — baseline the strict cursor on it.\n        self.baseline_next_output = from_seq == 0;\n        // A resume-mode brain (non-empty map) routes this session's output through\n        // the per-session map via `read_event`'s first-sight `or_insert` seed\n        // (D4-2b); a legacy brain (empty map) uses the `next_seq` set above. The\n        // resume-mode dedup-cursor reset for an explicit re-subscribe lives in\n        // `subscribe_with` (P1c, shared by `attach`/`attach_as`).\n        self.subscribe(session_id, from_seq)\n    }\n\n    /// Attach under the W2.5 controller/viewer model (REQ-RCVIEW-1): send a\n    /// role-aware subscribe with `intent` + the operator identity `by` (the\n    /// handshake-proven origin for a remote attach). The broker resolves the role\n    /// and replies a [`BrokerEvent::Subscribed`] the caller (a [`serve_attach`]\n    /// loop) reads to learn whether it became controller / viewer / was refused.\n    /// Like [`Brain::attach`] this is a second reader, not a logic handoff (the\n    /// generation is untouched).\n    ///\n    /// [`serve_attach`]: crate::attach::serve_attach\n    pub fn attach_as(\n        &mut self,\n        session_id: u64,\n        from_seq: u64,\n        intent: AttachIntent,\n        gen: u64,\n        by: Option<&str>,\n    ) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.next_seq = from_seq;\n        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n        // --attach` case) gets a clean-screen repaint at pseudo-seq\n        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n        // this false → raw-ring replay + strict reject-gap unchanged (B2).\n        self.baseline_next_output = from_seq == 0;\n        self.subscribe_with(session_id, from_seq, intent, gen, by.map(str::to_string))\n    }\n\n    /// Re-subscribe a VIEWER from the LIVE TAIL after a broker eviction —\n    /// skip-to-live (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). The viewer overflowed its\n    /// bounded subscription queue (it forwards slower than the drain fans out) and\n    /// the broker evicted it (correct session-protection); rather than dying it\n    /// jumps to live and skips the uncatchable backlog (tail -f reconnect).\n    ///\n    /// `from_seq = u64::MAX` makes the broker's ring replay (`s >= from_seq`)\n    /// EMPTY, so the viewer receives only the size frame + live output going\n    /// forward. The next live frame carries the current ring tail's seq — a\n    /// FORWARD JUMP from where this brain was evicted. The legacy `next_seq` dedup"}],"details":{"displayContent":{"text":"    /// 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_SPAWN_CONFLICT => {\n                    let ev: SpawnConflict = serde_json::from_value(env.payload)\n                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;\n                    return Ok(BrokerEvent::SpawnConflict {\n                        endpoint: ev.endpoint,\n…\n                    });\n                }\n…\n            }\n        }\n    }\n…\n    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.next_seq = from_seq;\n        // #6 (ADR-0031): a COLD attach receives a clean-screen repaint at pseudo-seq\n        // `broker.next_seq - 1`, not seq 0 — baseline the strict cursor on it.\n        self.baseline_next_output = from_seq == 0;\n        // A resume-mode brain (non-empty map) routes this session's output through\n        // the per-session map via `read_event`'s first-sight `or_insert` seed\n        // (D4-2b); a legacy brain (empty map) uses the `next_seq` set above. The\n        // resume-mode dedup-cursor reset for an explicit re-subscribe lives in\n        // `subscribe_with` (P1c, shared by `attach`/`attach_as`).\n        self.subscribe(session_id, from_seq)\n    }\n\n    /// Attach under the W2.5 controller/viewer model (REQ-RCVIEW-1): send a\n    /// role-aware subscribe with `intent` + the operator identity `by` (the\n    /// handshake-proven origin for a remote attach). The broker resolves the role\n    /// and replies a [`BrokerEvent::Subscribed`] the caller (a [`serve_attach`]\n    /// loop) reads to learn whether it became controller / viewer / was refused.\n    /// Like [`Brain::attach`] this is a second reader, not a logic handoff (the\n    /// generation is untouched).\n    ///\n    /// [`serve_attach`]: crate::attach::serve_attach\n    pub fn attach_as(\n        &mut self,\n        session_id: u64,\n        from_seq: u64,\n        intent: AttachIntent,\n        gen: u64,\n        by: Option<&str>,\n    ) -> io::Result<()> {\n        self.session_id = Some(session_id);\n        self.next_seq = from_seq;\n        // #6 (ADR-0031): a COLD attach (`from_seq == 0` — the `rc` / `endpoint run\n        // --attach` case) gets a clean-screen repaint at pseudo-seq\n        // `broker.next_seq - 1` as its first frame; baseline the strict cursor on it\n        // (accept-then-strict). A resume-from-floor re-fetch (`from_seq > 0`) leaves\n        // this false → raw-ring replay + strict reject-gap unchanged (B2).\n        self.baseline_next_output = from_seq == 0;\n        self.subscribe_with(session_id, from_seq, intent, gen, by.map(str::to_string))\n    }\n\n    /// Re-subscribe a VIEWER from the LIVE TAIL after a broker eviction —\n    /// skip-to-live (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). The viewer overflowed its\n    /// bounded subscription queue (it forwards slower than the drain fans out) and\n    /// the broker evicted it (correct session-protection); rather than dying it\n    /// jumps to live and skips the uncatchable backlog (tail -f reconnect).\n    ///\n    /// `from_seq = u64::MAX` makes the broker's ring replay (`s >= from_seq`)\n    /// EMPTY, so the viewer receives only the size frame + live output going\n    /// forward. The next live frame carries the current ring tail's seq — a\n    /// FORWARD JUMP from where this brain was evicted. The legacy `next_seq` dedup","startLine":680,"lineNumbers":[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,null,762,763,null,931,932,933,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\brain.rs"}}},"isError":false,"timestamp":1784365568114}}
{"type":"message","id":"1bde8a3e","parentId":"be617e2e","timestamp":"2026-07-18T09:06:08.121Z","message":{"role":"toolResult","toolCallId":"call_wfilJHw3y4NNXljmo9EZnyK7|fc_0ce5441d29b9f05d016a5b41fff7c8819b990b62358628928c","toolName":"read","content":[{"type":"text","text":"/// `kill` payload.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct KillReq {\n    /// Target session.\n    pub session_id: u64,\n}\n\n/// `output` event — a chunk of a session's PTY output (base64).\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct OutputEvent {\n    /// Originating session.\n    pub session_id: u64,\n    /// The broker-assigned, per-session monotonic sequence of this chunk. The\n    /// resume cursor: a reconnecting brain subscribes from its last seen\n    /// `seq + 1`, and dedups by ignoring any `seq` it has already processed.\n    pub seq: u64,\n    /// The raw output bytes, base64-encoded.\n    pub data_b64: String,\n}\n\n/// `exit` event — a session's child has been reaped.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ExitEvent {\n    /// Originating session.\n    pub session_id: u64,\n    /// The child's exit code, if known.\n    #[serde(default)]\n    pub code: Option<i32>,\n}\n\n/// `applied` event — an idempotent effect is durably applied or was already\n…\npub struct NetPresenceEvent {\n…\n    #[serde(default, skip_serializing_if = \"serde_json::Value::is_null\")]\n    pub remote_addr: serde_json::Value,\n}\n\n/// Encode raw bytes for an `input`/`output` payload.\npub fn encode_bytes(bytes: &[u8]) -> String {\n    B64.encode(bytes)\n}\n\n/// Decode an `input`/`output` payload's base64 back to raw bytes.\npub fn decode_bytes(s: &str) -> Result<Vec<u8>, base64::DecodeError> {\n    B64.decode(s)\n}\n\n/// Build an output event envelope for `session_id` / `seq` carrying `bytes`.\npub fn output_envelope(session_id: u64, seq: u64, bytes: &[u8]) -> Envelope {\n    Envelope::new(\n        KIND_OUTPUT,\n        serde_json::to_value(OutputEvent {\n            session_id,\n            seq,\n            data_b64: encode_bytes(bytes),\n        })\n        .expect(\"OutputEvent serializes\"),\n    )\n}\n\n/// Build a `subscribed` reply envelope carrying the resolved controller/viewer\n/// [`SubscribeOutcome`] for `session_id` (REQ-RCVIEW-1).\npub fn subscribed_envelope(session_id: u64, outcome: SubscribeOutcome) -> Envelope {\n    Envelope::new(\n…\n    )\n}"}],"details":{"displayContent":{"text":"/// `kill` payload.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct KillReq {\n    /// Target session.\n    pub session_id: u64,\n}\n\n/// `output` event — a chunk of a session's PTY output (base64).\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct OutputEvent {\n    /// Originating session.\n    pub session_id: u64,\n    /// The broker-assigned, per-session monotonic sequence of this chunk. The\n    /// resume cursor: a reconnecting brain subscribes from its last seen\n    /// `seq + 1`, and dedups by ignoring any `seq` it has already processed.\n    pub seq: u64,\n    /// The raw output bytes, base64-encoded.\n    pub data_b64: String,\n}\n\n/// `exit` event — a session's child has been reaped.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ExitEvent {\n    /// Originating session.\n    pub session_id: u64,\n    /// The child's exit code, if known.\n    #[serde(default)]\n    pub code: Option<i32>,\n}\n\n/// `applied` event — an idempotent effect is durably applied or was already\n…\npub struct NetPresenceEvent {\n…\n    #[serde(default, skip_serializing_if = \"serde_json::Value::is_null\")]\n    pub remote_addr: serde_json::Value,\n}\n\n/// Encode raw bytes for an `input`/`output` payload.\npub fn encode_bytes(bytes: &[u8]) -> String {\n    B64.encode(bytes)\n}\n\n/// Decode an `input`/`output` payload's base64 back to raw bytes.\npub fn decode_bytes(s: &str) -> Result<Vec<u8>, base64::DecodeError> {\n    B64.decode(s)\n}\n\n/// Build an output event envelope for `session_id` / `seq` carrying `bytes`.\npub fn output_envelope(session_id: u64, seq: u64, bytes: &[u8]) -> Envelope {\n    Envelope::new(\n        KIND_OUTPUT,\n        serde_json::to_value(OutputEvent {\n            session_id,\n            seq,\n            data_b64: encode_bytes(bytes),\n        })\n        .expect(\"OutputEvent serializes\"),\n    )\n}\n\n/// Build a `subscribed` reply envelope carrying the resolved controller/viewer\n/// [`SubscribeOutcome`] for `session_id` (REQ-RCVIEW-1).\npub fn subscribed_envelope(session_id: u64, outcome: SubscribeOutcome) -> Envelope {\n    Envelope::new(\n…\n    )\n}","startLine":540,"lineNumbers":[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,null,1135,null,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,null,1197,1198]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\msg.rs"}}},"isError":false,"timestamp":1784365568121}}
{"type":"message","id":"8961a84b","parentId":"1bde8a3e","timestamp":"2026-07-18T09:06:08.126Z","message":{"role":"toolResult","toolCallId":"call_twjv2OxobSGGoe3D3ytQZNnn|fc_0ce5441d29b9f05d016a5b41fff7c0819bb0cc53a1d3434ade","toolName":"read","content":[{"type":"text","text":"//! Remote terminal attach records (M4-D5b, REQ-INST-8) — the byte-stream\n//! viewport onto an instance running on another node.\n//!\n//! Remote-control is a **distinct mode from local operation** (CONTEXT\n//! §Remote-control vs local operation): compute + files stay on the target\n//! node; the operator's node is a viewport — a Shell-like driven surface,\n//! daemon-to-daemon over the broker-owned QUIC streams (D4). The records here\n//! are the wire shape of that viewport: the operator node opens a bidi stream,\n//! sends [`AttachRecord::Request`], and the target node pumps its session's\n//! `OutputLog` chunks back as [`AttachRecord::Output`] while injecting\n//! [`AttachRecord::Input`] into the PTY through the journaled exactly-once\n//! input path.\n//!\n//! Framing is the shared NDJSON ([`crate::net::ndjson`]); chunk bytes ride\n//! base64 inside the JSON (the same posture as the IPC catalog's\n//! `data_b64` fields — no binary fork of the codec). `Output.seq` is the\n//! target session's `OutputLog` sequence verbatim, so the operator side dedups\n//! replays (a target-brain restart redrives journaled sends) by cursor, and a\n//! restarted operator brain resubscribes the QUIC stream and skips what it\n//! already rendered — gapless exactly-once on both ends, composed entirely\n//! from the D4/B5 machinery.\n//!\n//! Like [`WanMessage`](crate::net::wanmsg::WanMessage), attach records carry\n//! **no origin field** — the access gate's subject is the QUIC handshake\n//! identity from the broker's stream table (KNOWN-HAZARDS 7.5,\n//! REQ-HAZARD-WAN-ORIGIN-AUTH).\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::net::ndjson::{self, NdjsonDecoder};\n\n/// What an operator's attach is *for* (REQ-RCVIEW-1 / REQ-KICK-1). Three-valued,\n/// not a binary role — a plain `Control` request to an already-controlled endpoint\n/// is REFUSED (never an auto-viewer, never a silent displace); explicit `Take` is\n/// the only way to displace the incumbent controller.\n///\n/// Wire default is [`AttachIntent::Control`] (`#[serde(default)]` on\n/// [`AttachRecord::Request`]): an N-1 operator that omits the field drives a FREE\n/// endpoint exactly as before, but — lacking the `Take` variant — can never\n/// silently steal a controlled one (it gets the busy-refusal reply instead).\n// [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum AttachIntent {\n    /// Read-only viewer: output only, no input, no resize. Never displaces a\n    /// controller; coexists with any number of other viewers.\n    Viewer,\n    /// Become the interactive controller — but only of a FREE endpoint; a\n    /// `Control` request to a controlled endpoint is refused with guidance.\n    #[default]\n    Control,\n    /// Kick the incumbent controller (loud [`AttachRecord::Displaced`] notice to\n    /// them) and take control in one motion (`spt rc --take` / picker Kick).\n    Take,\n}\n\n/// serde skip helper for `AttachRecord::Request::gen`: keep the wire\n/// byte-identical for local (gen-less) N-1 exchanges.\nfn gen_is_zero(g: &u64) -> bool {\n    *g == 0\n}\n\n/// One attach-protocol record. Tagged (`kind`) so both directions share one\n/// stream decoder; an unknown future kind fails the line's decode and is\n/// skipped (degrade, don't die — same posture as every wire feed). The\n/// W2.5 additions (`intent`, `Resize`, `Size`, `Displaced`) are additive: an\n/// N-1 peer omits `intent` (→ `Control`) and skips the unknown record kinds.\n// [impl->REQ-INST-8] [impl->REQ-RCVIEW-1]\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(tag = \"kind\", rename_all = \"snake_case\")]\npub enum AttachRecord {\n    /// Operator → target, first record on the stream: attach to `session_id`,\n    /// replaying output from `from_seq` (the operator's resume cursor — 0 for\n    /// a fresh attach, the last-rendered seq + 1 on re-attach). `intent` selects\n    /// viewer / control / take (defaulted to `Control` for N-1 operators).\n    ///\n    /// `endpoint_id` is the REMOTE cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    /// an operator on ANOTHER node cannot know the owning node's local\n    /// `session_id`, so it names the ENDPOINT and the owning node resolves\n    /// endpoint→its-own `session_id` SERVER-SIDE (it is authoritative for its own\n    /// session table — same subject-from-broker-table posture as the origin gate).\n    /// ABSENT on the LOCAL path, which sends a resolved `session_id`;\n    /// `skip_serializing_if` keeps the local wire byte-identical and N-1\n    /// compatible (an old target ignores the unknown field and reads `session_id`).\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    Request {\n        session_id: u64,\n        from_seq: u64,\n        #[serde(default)]\n        intent: AttachIntent,\n        #[serde(default, skip_serializing_if = \"Option::is_none\")]\n        endpoint_id: Option<String>,\n        /// Attach GENERATION (ADR-0038 Amendment fix 6,\n        /// REQ-SERVE-OWNERSHIP-GENERATION): operator-minted, monotonic per\n        /// viewport (wall-ms at attach). A REPLAYED stale Request carries a\n        /// generation OLDER than the live replacement controller's and is\n        /// refused the silent same-identity re-take; the SAME Request\n        /// re-served (dispatcher-restart reconstruction) carries an equal\n        /// generation and re-takes as before. `0` (absent — an N-1 operator)\n        /// keeps the legacy re-take behavior.\n        #[serde(default, skip_serializing_if = \"gen_is_zero\")]\n        gen: u64,\n    },\n    /// Target → operator: one session output chunk. `seq` is the target\n    /// session's `OutputLog` sequence verbatim (the operator's dedup cursor).\n    Output { seq: u64, data_b64: String },\n    /// Operator → target: input bytes for the session's PTY. `op_id` is the\n    /// operator-minted durable id the target feeds into its journaled\n    /// exactly-once input path (a replayed record cannot double-type). Honored\n    /// only from the CONTROLLER's stream (a viewer sends none).\n    Input { data_b64: String, op_id: u64 },\n    /// Operator(controller) → target: resize the PTY to the controller's viewport.\n    /// Controller-EXCLUSIVE — the broker rejects a `Resize` arriving on a viewer's\n    /// stream (REQ-RCVIEW-1, resize is controller-exclusive for ConPTY repaint cost).\n    Resize { rows: u16, cols: u16 },\n    /// Target → operator(viewer): the current PTY size, sent on a viewer's attach\n    /// and on every controller resize, so the viewer letterboxes client-side\n    /// (never resizing the PTY itself).\n    Size { rows: u16, cols: u16 },\n    /// Target → operator: the session's child exited; the viewport is done.\n    Exit { code: Option<i32> },\n    /// Target → operator(controller): you have been displaced as controller by\n    /// `by` (a `--take`/`kick`). The displaced controller renders this LOUD notice\n    /// and detaches — it is NOT demoted to a viewer (REQ-KICK-1).\n    Displaced { by: String },\n}\n\nimpl AttachRecord {\n    /// Encode as one NDJSON line (the JSON document + `\\n`).\n    pub fn encode_line(&self) -> Vec<u8> {\n        ndjson::encode_line(self)\n    }\n}\n\n/// The attach stream's NDJSON reassembler — the shared\n/// [`NdjsonDecoder`](crate::net::ndjson::NdjsonDecoder) typed to\n/// [`AttachRecord`]s. One decoder per stream side.\npub type AttachDecoder = NdjsonDecoder<AttachRecord>;\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // [unit->REQ-INST-8] every record kind round-trips through the NDJSON line\n    // encoding, and the decoder skips an unknown future kind without wedging\n    // the stream (forward compat: degrade, don't die).\n    #[test]\n    fn records_round_trip_and_unknown_kind_is_skipped() {\n        let records = vec![\n            AttachRecord::Request {\n                session_id: 7,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                endpoint_id: None,\n             gen: 0, },\n            AttachRecord::Output {\n                seq: 3,\n                data_b64: \"aGk=\".into(),\n            },\n            AttachRecord::Input {\n                data_b64: \"bHM=\".into(),\n                op_id: 42,\n            },\n            AttachRecord::Resize { rows: 40, cols: 120 },\n            AttachRecord::Size { rows: 40, cols: 120 },\n            AttachRecord::Exit { code: Some(0) },\n            AttachRecord::Displaced { by: \"hfenduleam\".into() },\n        ];\n        let mut wire = Vec::new();\n        for r in &records {\n            wire.extend_from_slice(&r.encode_line());\n        }\n        // An unknown future kind in the middle of the feed is skipped.\n        let mut with_junk = records[0].encode_line();\n        with_junk.extend_from_slice(b\"{\\\"kind\\\":\\\"added_at_d9\\\",\\\"x\\\":1}\\n\");\n        with_junk.extend_from_slice(&records[3].encode_line());\n\n        let mut dec = AttachDecoder::new();\n        assert_eq!(dec.push(&wire), records, \"all kinds round-trip in order\");\n\n        let mut dec = AttachDecoder::new();\n        assert_eq!(\n            dec.push(&with_junk),\n            vec![records[0].clone(), records[3].clone()],\n            \"unknown kind skipped, feed not wedged\"\n        );\n    }\n\n    // [unit->REQ-INST-8] chunk-split safety rides the shared decoder: a record\n    // split mid-bytes across pushes reassembles (QUIC chunking is not record\n    // framing).\n    #[test]\n    fn record_split_across_chunks_reassembles() {\n        let r = AttachRecord::Output {\n            seq: 9,\n            data_b64: \"c3BsaXQtbWU=\".into(),\n        };\n        let line = r.encode_line();\n        let mut dec = AttachDecoder::new();\n        assert!(dec.push(&line[..line.len() / 2]).is_empty());\n        assert_eq!(dec.push(&line[line.len() / 2..]), vec![r]);\n    }\n\n    // [unit->REQ-RCVIEW-1] N-1 compat: an attach Request encoded WITHOUT the\n    // `intent` field (an old operator) decodes to `Control` — it drives a free\n    // endpoint as before, and, lacking the Take variant, can never silently\n    // steal a controlled one (it gets the busy-refusal instead).\n    #[test]\n    fn request_without_intent_defaults_to_control() {\n        let mut dec = AttachDecoder::new();\n        let legacy = b\"{\\\"kind\\\":\\\"request\\\",\\\"session_id\\\":7,\\\"from_seq\\\":0}\\n\";\n        assert_eq!(\n            dec.push(legacy),\n            vec![AttachRecord::Request {\n                session_id: 7,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                endpoint_id: None,\n             gen: 0, }],\n            \"omitted intent defaults to Control (N-1 operator)\"\n        );\n        assert_eq!(AttachIntent::default(), AttachIntent::Control);\n    }\n…\n}\n\n[Showing lines 1-223 of 290. Use :224 to continue]"}],"details":{"truncation":{"content":"//! Remote terminal attach records (M4-D5b, REQ-INST-8) — the byte-stream\n//! viewport onto an instance running on another node.\n//!\n//! Remote-control is a **distinct mode from local operation** (CONTEXT\n//! §Remote-control vs local operation): compute + files stay on the target\n//! node; the operator's node is a viewport — a Shell-like driven surface,\n//! daemon-to-daemon over the broker-owned QUIC streams (D4). The records here\n//! are the wire shape of that viewport: the operator node opens a bidi stream,\n//! sends [`AttachRecord::Request`], and the target node pumps its session's\n//! `OutputLog` chunks back as [`AttachRecord::Output`] while injecting\n//! [`AttachRecord::Input`] into the PTY through the journaled exactly-once\n//! input path.\n//!\n//! Framing is the shared NDJSON ([`crate::net::ndjson`]); chunk bytes ride\n//! base64 inside the JSON (the same posture as the IPC catalog's\n//! `data_b64` fields — no binary fork of the codec). `Output.seq` is the\n//! target session's `OutputLog` sequence verbatim, so the operator side dedups\n//! replays (a target-brain restart redrives journaled sends) by cursor, and a\n//! restarted operator brain resubscribes the QUIC stream and skips what it\n//! already rendered — gapless exactly-once on both ends, composed entirely\n//! from the D4/B5 machinery.\n//!\n//! Like [`WanMessage`](crate::net::wanmsg::WanMessage), attach records carry\n//! **no origin field** — the access gate's subject is the QUIC handshake\n//! identity from the broker's stream table (KNOWN-HAZARDS 7.5,\n//! REQ-HAZARD-WAN-ORIGIN-AUTH).\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::net::ndjson::{self, NdjsonDecoder};\n\n/// What an operator's attach is *for* (REQ-RCVIEW-1 / REQ-KICK-1). Three-valued,\n/// not a binary role — a plain `Control` request to an already-controlled endpoint\n/// is REFUSED (never an auto-viewer, never a silent displace); explicit `Take` is\n/// the only way to displace the incumbent controller.\n///\n/// Wire default is [`AttachIntent::Control`] (`#[serde(default)]` on\n/// [`AttachRecord::Request`]): an N-1 operator that omits the field drives a FREE\n/// endpoint exactly as before, but — lacking the `Take` variant — can never\n/// silently steal a controlled one (it gets the busy-refusal reply instead).\n// [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum AttachIntent {\n    /// Read-only viewer: output only, no input, no resize. Never displaces a\n    /// controller; coexists with any number of other viewers.\n    Viewer,\n    /// Become the interactive controller — but only of a FREE endpoint; a\n    /// `Control` request to a controlled endpoint is refused with guidance.\n    #[default]\n    Control,\n    /// Kick the incumbent controller (loud [`AttachRecord::Displaced`] notice to\n    /// them) and take control in one motion (`spt rc --take` / picker Kick).\n    Take,\n}\n\n/// serde skip helper for `AttachRecord::Request::gen`: keep the wire\n/// byte-identical for local (gen-less) N-1 exchanges.\nfn gen_is_zero(g: &u64) -> bool {\n    *g == 0\n}\n\n/// One attach-protocol record. Tagged (`kind`) so both directions share one\n/// stream decoder; an unknown future kind fails the line's decode and is\n/// skipped (degrade, don't die — same posture as every wire feed). The\n/// W2.5 additions (`intent`, `Resize`, `Size`, `Displaced`) are additive: an\n/// N-1 peer omits `intent` (→ `Control`) and skips the unknown record kinds.\n// [impl->REQ-INST-8] [impl->REQ-RCVIEW-1]\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(tag = \"kind\", rename_all = \"snake_case\")]\npub enum AttachRecord {\n    /// Operator → target, first record on the stream: attach to `session_id`,\n    /// replaying output from `from_seq` (the operator's resume cursor — 0 for\n    /// a fresh attach, the last-rendered seq + 1 on re-attach). `intent` selects\n    /// viewer / control / take (defaulted to `Control` for N-1 operators).\n    ///\n    /// `endpoint_id` is the REMOTE cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    /// an operator on ANOTHER node cannot know the owning node's local\n    /// `session_id`, so it names the ENDPOINT and the owning node resolves\n    /// endpoint→its-own `session_id` SERVER-SIDE (it is authoritative for its own\n    /// session table — same subject-from-broker-table posture as the origin gate).\n    /// ABSENT on the LOCAL path, which sends a resolved `session_id`;\n    /// `skip_serializing_if` keeps the local wire byte-identical and N-1\n    /// compatible (an old target ignores the unknown field and reads `session_id`).\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    Request {\n        session_id: u64,\n        from_seq: u64,\n        #[serde(default)]\n        intent: AttachIntent,\n        #[serde(default, skip_serializing_if = \"Option::is_none\")]\n        endpoint_id: Option<String>,\n        /// Attach GENERATION (ADR-0038 Amendment fix 6,\n        /// REQ-SERVE-OWNERSHIP-GENERATION): operator-minted, monotonic per\n        /// viewport (wall-ms at attach). A REPLAYED stale Request carries a\n        /// generation OLDER than the live replacement controller's and is\n        /// refused the silent same-identity re-take; the SAME Request\n        /// re-served (dispatcher-restart reconstruction) carries an equal\n        /// generation and re-takes as before. `0` (absent — an N-1 operator)\n        /// keeps the legacy re-take behavior.\n        #[serde(default, skip_serializing_if = \"gen_is_zero\")]\n        gen: u64,\n    },\n    /// Target → operator: one session output chunk. `seq` is the target\n    /// session's `OutputLog` sequence verbatim (the operator's dedup cursor).\n    Output { seq: u64, data_b64: String },\n    /// Operator → target: input bytes for the session's PTY. `op_id` is the\n    /// operator-minted durable id the target feeds into its journaled\n    /// exactly-once input path (a replayed record cannot double-type). Honored\n    /// only from the CONTROLLER's stream (a viewer sends none).\n    Input { data_b64: String, op_id: u64 },\n    /// Operator(controller) → target: resize the PTY to the controller's viewport.\n    /// Controller-EXCLUSIVE — the broker rejects a `Resize` arriving on a viewer's\n    /// stream (REQ-RCVIEW-1, resize is controller-exclusive for ConPTY repaint cost).\n    Resize { rows: u16, cols: u16 },\n    /// Target → operator(viewer): the current PTY size, sent on a viewer's attach\n    /// and on every controller resize, so the viewer letterboxes client-side\n    /// (never resizing the PTY itself).\n    Size { rows: u16, cols: u16 },\n    /// Target → operator: the session's child exited; the viewport is done.\n    Exit { code: Option<i32> },\n    /// Target → operator(controller): you have been displaced as controller by\n    /// `by` (a `--take`/`kick`). The displaced controller renders this LOUD notice\n    /// and detaches — it is NOT demoted to a viewer (REQ-KICK-1).\n    Displaced { by: String },\n}\n\nimpl AttachRecord {\n    /// Encode as one NDJSON line (the JSON document + `\\n`).\n    pub fn encode_line(&self) -> Vec<u8> {\n        ndjson::encode_line(self)\n    }\n}\n\n/// The attach stream's NDJSON reassembler — the shared\n/// [`NdjsonDecoder`](crate::net::ndjson::NdjsonDecoder) typed to\n/// [`AttachRecord`]s. One decoder per stream side.\npub type AttachDecoder = NdjsonDecoder<AttachRecord>;\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // [unit->REQ-INST-8] every record kind round-trips through the NDJSON line\n    // encoding, and the decoder skips an unknown future kind without wedging\n    // the stream (forward compat: degrade, don't die).\n    #[test]\n    fn records_round_trip_and_unknown_kind_is_skipped() {\n        let records = vec![\n            AttachRecord::Request {\n                session_id: 7,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                endpoint_id: None,\n             gen: 0, },\n            AttachRecord::Output {\n                seq: 3,\n                data_b64: \"aGk=\".into(),\n            },\n            AttachRecord::Input {\n                data_b64: \"bHM=\".into(),\n                op_id: 42,\n            },\n            AttachRecord::Resize { rows: 40, cols: 120 },\n            AttachRecord::Size { rows: 40, cols: 120 },\n            AttachRecord::Exit { code: Some(0) },\n            AttachRecord::Displaced { by: \"hfenduleam\".into() },\n        ];\n        let mut wire = Vec::new();\n        for r in &records {\n            wire.extend_from_slice(&r.encode_line());\n        }\n        // An unknown future kind in the middle of the feed is skipped.\n        let mut with_junk = records[0].encode_line();\n        with_junk.extend_from_slice(b\"{\\\"kind\\\":\\\"added_at_d9\\\",\\\"x\\\":1}\\n\");\n        with_junk.extend_from_slice(&records[3].encode_line());\n\n        let mut dec = AttachDecoder::new();\n        assert_eq!(dec.push(&wire), records, \"all kinds round-trip in order\");\n\n        let mut dec = AttachDecoder::new();\n        assert_eq!(\n            dec.push(&with_junk),\n            vec![records[0].clone(), records[3].clone()],\n            \"unknown kind skipped, feed not wedged\"\n        );\n    }\n\n    // [unit->REQ-INST-8] chunk-split safety rides the shared decoder: a record\n    // split mid-bytes across pushes reassembles (QUIC chunking is not record\n    // framing).\n    #[test]\n    fn record_split_across_chunks_reassembles() {\n        let r = AttachRecord::Output {\n            seq: 9,\n            data_b64: \"c3BsaXQtbWU=\".into(),\n        };\n        let line = r.encode_line();\n        let mut dec = AttachDecoder::new();\n        assert!(dec.push(&line[..line.len() / 2]).is_empty());\n        assert_eq!(dec.push(&line[line.len() / 2..]), vec![r]);\n    }\n\n    // [unit->REQ-RCVIEW-1] N-1 compat: an attach Request encoded WITHOUT the\n    // `intent` field (an old operator) decodes to `Control` — it drives a free\n    // endpoint as before, and, lacking the Take variant, can never silently\n    // steal a controlled one (it gets the busy-refusal instead).\n    #[test]\n    fn request_without_intent_defaults_to_control() {\n        let mut dec = AttachDecoder::new();\n        let legacy = b\"{\\\"kind\\\":\\\"request\\\",\\\"session_id\\\":7,\\\"from_seq\\\":0}\\n\";\n        assert_eq!(\n            dec.push(legacy),\n            vec![AttachRecord::Request {\n                session_id: 7,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                endpoint_id: None,\n             gen: 0, }],\n            \"omitted intent defaults to Control (N-1 operator)\"\n        );\n        assert_eq!(AttachIntent::default(), AttachIntent::Control);\n    }","truncated":true,"truncatedBy":"lines","totalLines":290,"totalBytes":10676,"outputLines":223,"outputBytes":10676,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Remote terminal attach records (M4-D5b, REQ-INST-8) — the byte-stream\n//! viewport onto an instance running on another node.\n//!\n//! Remote-control is a **distinct mode from local operation** (CONTEXT\n//! §Remote-control vs local operation): compute + files stay on the target\n//! node; the operator's node is a viewport — a Shell-like driven surface,\n//! daemon-to-daemon over the broker-owned QUIC streams (D4). The records here\n//! are the wire shape of that viewport: the operator node opens a bidi stream,\n//! sends [`AttachRecord::Request`], and the target node pumps its session's\n//! `OutputLog` chunks back as [`AttachRecord::Output`] while injecting\n//! [`AttachRecord::Input`] into the PTY through the journaled exactly-once\n//! input path.\n//!\n//! Framing is the shared NDJSON ([`crate::net::ndjson`]); chunk bytes ride\n//! base64 inside the JSON (the same posture as the IPC catalog's\n//! `data_b64` fields — no binary fork of the codec). `Output.seq` is the\n//! target session's `OutputLog` sequence verbatim, so the operator side dedups\n//! replays (a target-brain restart redrives journaled sends) by cursor, and a\n//! restarted operator brain resubscribes the QUIC stream and skips what it\n//! already rendered — gapless exactly-once on both ends, composed entirely\n//! from the D4/B5 machinery.\n//!\n//! Like [`WanMessage`](crate::net::wanmsg::WanMessage), attach records carry\n//! **no origin field** — the access gate's subject is the QUIC handshake\n//! identity from the broker's stream table (KNOWN-HAZARDS 7.5,\n//! REQ-HAZARD-WAN-ORIGIN-AUTH).\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::net::ndjson::{self, NdjsonDecoder};\n\n/// What an operator's attach is *for* (REQ-RCVIEW-1 / REQ-KICK-1). Three-valued,\n/// not a binary role — a plain `Control` request to an already-controlled endpoint\n/// is REFUSED (never an auto-viewer, never a silent displace); explicit `Take` is\n/// the only way to displace the incumbent controller.\n///\n/// Wire default is [`AttachIntent::Control`] (`#[serde(default)]` on\n/// [`AttachRecord::Request`]): an N-1 operator that omits the field drives a FREE\n/// endpoint exactly as before, but — lacking the `Take` variant — can never\n/// silently steal a controlled one (it gets the busy-refusal reply instead).\n// [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum AttachIntent {\n    /// Read-only viewer: output only, no input, no resize. Never displaces a\n    /// controller; coexists with any number of other viewers.\n    Viewer,\n    /// Become the interactive controller — but only of a FREE endpoint; a\n    /// `Control` request to a controlled endpoint is refused with guidance.\n    #[default]\n    Control,\n    /// Kick the incumbent controller (loud [`AttachRecord::Displaced`] notice to\n    /// them) and take control in one motion (`spt rc --take` / picker Kick).\n    Take,\n}\n\n/// serde skip helper for `AttachRecord::Request::gen`: keep the wire\n/// byte-identical for local (gen-less) N-1 exchanges.\nfn gen_is_zero(g: &u64) -> bool {\n    *g == 0\n}\n\n/// One attach-protocol record. Tagged (`kind`) so both directions share one\n/// stream decoder; an unknown future kind fails the line's decode and is\n/// skipped (degrade, don't die — same posture as every wire feed). The\n/// W2.5 additions (`intent`, `Resize`, `Size`, `Displaced`) are additive: an\n/// N-1 peer omits `intent` (→ `Control`) and skips the unknown record kinds.\n// [impl->REQ-INST-8] [impl->REQ-RCVIEW-1]\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(tag = \"kind\", rename_all = \"snake_case\")]\npub enum AttachRecord {\n    /// Operator → target, first record on the stream: attach to `session_id`,\n    /// replaying output from `from_seq` (the operator's resume cursor — 0 for\n    /// a fresh attach, the last-rendered seq + 1 on re-attach). `intent` selects\n    /// viewer / control / take (defaulted to `Control` for N-1 operators).\n    ///\n    /// `endpoint_id` is the REMOTE cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH):\n    /// an operator on ANOTHER node cannot know the owning node's local\n    /// `session_id`, so it names the ENDPOINT and the owning node resolves\n    /// endpoint→its-own `session_id` SERVER-SIDE (it is authoritative for its own\n    /// session table — same subject-from-broker-table posture as the origin gate).\n    /// ABSENT on the LOCAL path, which sends a resolved `session_id`;\n    /// `skip_serializing_if` keeps the local wire byte-identical and N-1\n    /// compatible (an old target ignores the unknown field and reads `session_id`).\n    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n    Request {\n        session_id: u64,\n        from_seq: u64,\n        #[serde(default)]\n        intent: AttachIntent,\n        #[serde(default, skip_serializing_if = \"Option::is_none\")]\n        endpoint_id: Option<String>,\n        /// Attach GENERATION (ADR-0038 Amendment fix 6,\n        /// REQ-SERVE-OWNERSHIP-GENERATION): operator-minted, monotonic per\n        /// viewport (wall-ms at attach). A REPLAYED stale Request carries a\n        /// generation OLDER than the live replacement controller's and is\n        /// refused the silent same-identity re-take; the SAME Request\n        /// re-served (dispatcher-restart reconstruction) carries an equal\n        /// generation and re-takes as before. `0` (absent — an N-1 operator)\n        /// keeps the legacy re-take behavior.\n        #[serde(default, skip_serializing_if = \"gen_is_zero\")]\n        gen: u64,\n    },\n    /// Target → operator: one session output chunk. `seq` is the target\n    /// session's `OutputLog` sequence verbatim (the operator's dedup cursor).\n    Output { seq: u64, data_b64: String },\n    /// Operator → target: input bytes for the session's PTY. `op_id` is the\n    /// operator-minted durable id the target feeds into its journaled\n    /// exactly-once input path (a replayed record cannot double-type). Honored\n    /// only from the CONTROLLER's stream (a viewer sends none).\n    Input { data_b64: String, op_id: u64 },\n    /// Operator(controller) → target: resize the PTY to the controller's viewport.\n    /// Controller-EXCLUSIVE — the broker rejects a `Resize` arriving on a viewer's\n    /// stream (REQ-RCVIEW-1, resize is controller-exclusive for ConPTY repaint cost).\n    Resize { rows: u16, cols: u16 },\n    /// Target → operator(viewer): the current PTY size, sent on a viewer's attach\n    /// and on every controller resize, so the viewer letterboxes client-side\n    /// (never resizing the PTY itself).\n    Size { rows: u16, cols: u16 },\n    /// Target → operator: the session's child exited; the viewport is done.\n    Exit { code: Option<i32> },\n    /// Target → operator(controller): you have been displaced as controller by\n    /// `by` (a `--take`/`kick`). The displaced controller renders this LOUD notice\n    /// and detaches — it is NOT demoted to a viewer (REQ-KICK-1).\n    Displaced { by: String },\n}\n\nimpl AttachRecord {\n    /// Encode as one NDJSON line (the JSON document + `\\n`).\n    pub fn encode_line(&self) -> Vec<u8> {\n        ndjson::encode_line(self)\n    }\n}\n\n/// The attach stream's NDJSON reassembler — the shared\n/// [`NdjsonDecoder`](crate::net::ndjson::NdjsonDecoder) typed to\n/// [`AttachRecord`]s. One decoder per stream side.\npub type AttachDecoder = NdjsonDecoder<AttachRecord>;\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // [unit->REQ-INST-8] every record kind round-trips through the NDJSON line\n    // encoding, and the decoder skips an unknown future kind without wedging\n    // the stream (forward compat: degrade, don't die).\n    #[test]\n    fn records_round_trip_and_unknown_kind_is_skipped() {\n        let records = vec![\n            AttachRecord::Request {\n                session_id: 7,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                endpoint_id: None,\n             gen: 0, },\n            AttachRecord::Output {\n                seq: 3,\n                data_b64: \"aGk=\".into(),\n            },\n            AttachRecord::Input {\n                data_b64: \"bHM=\".into(),\n                op_id: 42,\n            },\n            AttachRecord::Resize { rows: 40, cols: 120 },\n            AttachRecord::Size { rows: 40, cols: 120 },\n            AttachRecord::Exit { code: Some(0) },\n            AttachRecord::Displaced { by: \"hfenduleam\".into() },\n        ];\n        let mut wire = Vec::new();\n        for r in &records {\n            wire.extend_from_slice(&r.encode_line());\n        }\n        // An unknown future kind in the middle of the feed is skipped.\n        let mut with_junk = records[0].encode_line();\n        with_junk.extend_from_slice(b\"{\\\"kind\\\":\\\"added_at_d9\\\",\\\"x\\\":1}\\n\");\n        with_junk.extend_from_slice(&records[3].encode_line());\n\n        let mut dec = AttachDecoder::new();\n        assert_eq!(dec.push(&wire), records, \"all kinds round-trip in order\");\n\n        let mut dec = AttachDecoder::new();\n        assert_eq!(\n            dec.push(&with_junk),\n            vec![records[0].clone(), records[3].clone()],\n            \"unknown kind skipped, feed not wedged\"\n        );\n    }\n\n    // [unit->REQ-INST-8] chunk-split safety rides the shared decoder: a record\n    // split mid-bytes across pushes reassembles (QUIC chunking is not record\n    // framing).\n    #[test]\n    fn record_split_across_chunks_reassembles() {\n        let r = AttachRecord::Output {\n            seq: 9,\n            data_b64: \"c3BsaXQtbWU=\".into(),\n        };\n        let line = r.encode_line();\n        let mut dec = AttachDecoder::new();\n        assert!(dec.push(&line[..line.len() / 2]).is_empty());\n        assert_eq!(dec.push(&line[line.len() / 2..]), vec![r]);\n    }\n\n    // [unit->REQ-RCVIEW-1] N-1 compat: an attach Request encoded WITHOUT the\n    // `intent` field (an old operator) decodes to `Control` — it drives a free\n    // endpoint as before, and, lacking the Take variant, can never silently\n    // steal a controlled one (it gets the busy-refusal instead).\n    #[test]\n    fn request_without_intent_defaults_to_control() {\n        let mut dec = AttachDecoder::new();\n        let legacy = b\"{\\\"kind\\\":\\\"request\\\",\\\"session_id\\\":7,\\\"from_seq\\\":0}\\n\";\n        assert_eq!(\n            dec.push(legacy),\n            vec![AttachRecord::Request {\n                session_id: 7,\n                from_seq: 0,\n                intent: AttachIntent::Control,\n                endpoint_id: None,\n             gen: 0, }],\n            \"omitted intent defaults to Control (N-1 operator)\"\n        );\n        assert_eq!(AttachIntent::default(), AttachIntent::Control);\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,null,289]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-net\\src\\net\\attach.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":290,"totalBytes":10676,"outputLines":223,"outputBytes":10676,"shownRange":{"start":1,"end":223},"nextOffset":224}}},"isError":false,"timestamp":1784365568126}}
{"type":"message","id":"e2542aa8","parentId":"8961a84b","timestamp":"2026-07-18T09:06:08.133Z","message":{"role":"toolResult","toolCallId":"call_KhJK4tl1gZs7Wrdg3YwCFetr|fc_0ce5441d29b9f05d016a5b41fff7d4819b98b422930089324b","toolName":"read","content":[{"type":"text","text":"impl Inner {\n…\n    fn watchdog(&self) {\n…\n    }\n}\n\n/// The shared, serialized, BOUNDED send side of one physical broker\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\npub(crate) struct BrokerConn {\n    inner: Arc<Inner>,\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\n    /// watchdog aborts through strictly outlives every raw use.\n    dog: Option<JoinHandle<()>>,\n}\n\nimpl BrokerConn {\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\n    /// deadline (gate-wait + OS write completion) — the broker passes its\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n        let raw = raw_of(&half);\n        let inner = Arc::new(Inner {\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n            label: Mutex::new(String::new()),\n            first_write_logged: AtomicBool::new(false),\n            gate: Mutex::new(Gate { half: Some(half) }),\n            gate_cv: Condvar::new(),\n            dog: Mutex::new(Dog {\n                inflight: None,\n                shutdown: false,\n            }),\n            dog_cv: Condvar::new(),\n            poisoned: AtomicBool::new(false),\n            timed_out: AtomicBool::new(false),\n            severed: AtomicBool::new(false),\n            graveyard: Mutex::new(None),\n            op_seq: AtomicU64::new(0),\n            bound,\n            raw,\n        });\n        let dog_inner = Arc::clone(&inner);\n        let dog = thread::Builder::new()\n            .name(\"conn-watchdog\".into())\n            .spawn(move || dog_inner.watchdog())\n            .ok();\n        BrokerConn { inner, dog }\n    }\n\n    /// The stable per-physical-conn id — what lifecycle records key on\n    /// (`Arc::ptr_eq` identity does not survive a log line).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn id(&self) -> u64 {\n        self.inner.id\n    }\n\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\n    /// endpoint/session where known — e.g. `role=brain`,\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn describe(&self, fact: &str) {\n        let mut l = recover(&self.inner.label);\n        if l.ends_with('…') {\n            return;\n        }\n        if l.len() + fact.len() + 1 > LABEL_CAP {\n            l.push('…');\n            return;\n        }\n        if !l.is_empty() {\n            l.push(' ');\n        }\n        l.push_str(fact);\n    }\n\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\n    /// attribution format — the broker's sink machinery uses it for\n    /// `writer-exit`, attach/replace, and detach events (leg d).\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\n        self.inner.lifecycle(event, extra);\n    }\n\n    /// Bounded framed write. See [`Self::write_gated`].\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\n        self.write_gated(env, || true)\n    }\n\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\n    /// serialized gate is acquired and BEFORE any byte is written — the same\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\n    /// superseded controller writer still bails atomically with respect to\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\n    pub(crate) fn write_gated(\n        &self,\n        env: &Envelope,\n        pre: impl FnOnce() -> bool,\n    ) -> io::Result<ConnWrite> {\n        let inner = &*self.inner;\n        let deadline = Instant::now() + inner.bound;\n\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\n        let mut g = recover(&inner.gate);\n        loop {\n            if inner.poisoned.load(Ordering::Acquire) {\n                return Err(poisoned_err());\n            }\n            if g.half.is_some() {\n                break;\n            }\n            let now = Instant::now();\n            if now >= deadline {\n                // The write we queued behind is stuck past OUR whole bound:\n                // that op is the wedge. Abort it out of band and fail — the\n                // watchdog's own deadline (armed at that op's entry) is also\n                // in flight; both paths converge on the same idempotent\n                // poison/cancel.\n                drop(g);\n                inner.timed_out.store(true, Ordering::Release);\n                inner.poison_and_cancel(None);\n                return Err(timeout_err());\n            }\n            let (g2, _) = inner\n                .gate_cv\n                .wait_timeout(g, deadline - now)\n                .unwrap_or_else(|p| p.into_inner());\n            g = g2;\n        }\n\n        if !pre() {\n            return Ok(ConnWrite::Superseded);\n        }\n\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\n        // first committed write, so a conn that later retires can be correlated\n        // to when it went active (fresh-carrier churn shows as open→start→retire\n        // triplets with fresh ids).\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\n            inner.lifecycle(\"write-start\", \"\");\n        }\n\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = Some((op, deadline));\n            inner.dog_cv.notify_all();\n        }\n        drop(g);\n\n        let res = write_frame(&mut half, env);\n\n        // Completion handshake: `write_frame` has returned, so the OS op —\n        // canceled or not — reported completion and the frame buffer is\n        // released. Clear the arm BEFORE deciding health, so the watchdog\n        // never severs while the op is formally in flight.\n        {\n            let mut d = recover(&inner.dog);\n            d.inflight = None;\n            inner.dog_cv.notify_all();\n        }\n\n        let mut g = recover(&inner.gate);\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\n            g.half = Some(half);\n            drop(g);\n            inner.gate_cv.notify_all();\n            return Ok(ConnWrite::Done);\n        }\n\n        // ── Retire: poison the whole conn, abort the read side so\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\n        // graveyard (its handle must outlive any late raw abort; never\n        // re-gated, never reused — a timed-out length-prefixed frame may be\n        // partially written). ──\n        drop(g);\n        inner.poison_and_cancel(res.as_ref().err());\n        inner.sever();\n        *recover(&inner.graveyard) = Some(half);\n        inner.gate_cv.notify_all();\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\n        // (peer death) keeps its own error.\n        if inner.timed_out.load(Ordering::Acquire) {\n            return Err(timeout_err());\n        }\n        Err(match res {\n            Err(e) => e,\n            Ok(()) => poisoned_err(),\n        })\n    }\n}\n\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\n/// window (the P1c epoch-gate keystone unit relies on it).\n#[cfg(test)]\npub(crate) struct TestGatePin {\n    inner: Arc<Inner>,\n    half: Option<SendHalf>,\n}\n\n#[cfg(test)]\nimpl Drop for TestGatePin {\n    fn drop(&mut self) {\n        let mut g = recover(&self.inner.gate);\n        g.half = self.half.take();\n        drop(g);\n        self.inner.gate_cv.notify_all();\n    }\n}\n\n\n[Showing lines 409-623 of 815. Use :624 to continue]"}],"details":{"truncation":{"content":"    }\r\n}\r\n\r\n/// The shared, serialized, BOUNDED send side of one physical broker\r\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\r\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\r\npub(crate) struct BrokerConn {\r\n    inner: Arc<Inner>,\r\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\r\n    /// watchdog aborts through strictly outlives every raw use.\r\n    dog: Option<JoinHandle<()>>,\r\n}\r\n\r\nimpl BrokerConn {\r\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\r\n    /// deadline (gate-wait + OS write completion) — the broker passes its\r\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\r\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\r\n        let raw = raw_of(&half);\r\n        let inner = Arc::new(Inner {\r\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\r\n            label: Mutex::new(String::new()),\r\n            first_write_logged: AtomicBool::new(false),\r\n            gate: Mutex::new(Gate { half: Some(half) }),\r\n            gate_cv: Condvar::new(),\r\n            dog: Mutex::new(Dog {\r\n                inflight: None,\r\n                shutdown: false,\r\n            }),\r\n            dog_cv: Condvar::new(),\r\n            poisoned: AtomicBool::new(false),\r\n            timed_out: AtomicBool::new(false),\r\n            severed: AtomicBool::new(false),\r\n            graveyard: Mutex::new(None),\r\n            op_seq: AtomicU64::new(0),\r\n            bound,\r\n            raw,\r\n        });\r\n        let dog_inner = Arc::clone(&inner);\r\n        let dog = thread::Builder::new()\r\n            .name(\"conn-watchdog\".into())\r\n            .spawn(move || dog_inner.watchdog())\r\n            .ok();\r\n        BrokerConn { inner, dog }\r\n    }\r\n\r\n    /// The stable per-physical-conn id — what lifecycle records key on\r\n    /// (`Arc::ptr_eq` identity does not survive a log line).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn id(&self) -> u64 {\r\n        self.inner.id\r\n    }\r\n\r\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\r\n    /// endpoint/session where known — e.g. `role=brain`,\r\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\r\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\r\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn describe(&self, fact: &str) {\r\n        let mut l = recover(&self.inner.label);\r\n        if l.ends_with('…') {\r\n            return;\r\n        }\r\n        if l.len() + fact.len() + 1 > LABEL_CAP {\r\n            l.push('…');\r\n            return;\r\n        }\r\n        if !l.is_empty() {\r\n            l.push(' ');\r\n        }\r\n        l.push_str(fact);\r\n    }\r\n\r\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\r\n    /// attribution format — the broker's sink machinery uses it for\r\n    /// `writer-exit`, attach/replace, and detach events (leg d).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\r\n        self.inner.lifecycle(event, extra);\r\n    }\r\n\r\n    /// Bounded framed write. See [`Self::write_gated`].\r\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\r\n        self.write_gated(env, || true)\r\n    }\r\n\r\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\r\n    /// serialized gate is acquired and BEFORE any byte is written — the same\r\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\r\n    /// superseded controller writer still bails atomically with respect to\r\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\r\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\r\n    pub(crate) fn write_gated(\r\n        &self,\r\n        env: &Envelope,\r\n        pre: impl FnOnce() -> bool,\r\n    ) -> io::Result<ConnWrite> {\r\n        let inner = &*self.inner;\r\n        let deadline = Instant::now() + inner.bound;\r\n\r\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\r\n        let mut g = recover(&inner.gate);\r\n        loop {\r\n            if inner.poisoned.load(Ordering::Acquire) {\r\n                return Err(poisoned_err());\r\n            }\r\n            if g.half.is_some() {\r\n                break;\r\n            }\r\n            let now = Instant::now();\r\n            if now >= deadline {\r\n                // The write we queued behind is stuck past OUR whole bound:\r\n                // that op is the wedge. Abort it out of band and fail — the\r\n                // watchdog's own deadline (armed at that op's entry) is also\r\n                // in flight; both paths converge on the same idempotent\r\n                // poison/cancel.\r\n                drop(g);\r\n                inner.timed_out.store(true, Ordering::Release);\r\n                inner.poison_and_cancel(None);\r\n                return Err(timeout_err());\r\n            }\r\n            let (g2, _) = inner\r\n                .gate_cv\r\n                .wait_timeout(g, deadline - now)\r\n                .unwrap_or_else(|p| p.into_inner());\r\n            g = g2;\r\n        }\r\n\r\n        if !pre() {\r\n            return Ok(ConnWrite::Superseded);\r\n        }\r\n\r\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\r\n        // first committed write, so a conn that later retires can be correlated\r\n        // to when it went active (fresh-carrier churn shows as open→start→retire\r\n        // triplets with fresh ids).\r\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\r\n            inner.lifecycle(\"write-start\", \"\");\r\n        }\r\n\r\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\r\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\r\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = Some((op, deadline));\r\n            inner.dog_cv.notify_all();\r\n        }\r\n        drop(g);\r\n\r\n        let res = write_frame(&mut half, env);\r\n\r\n        // Completion handshake: `write_frame` has returned, so the OS op —\r\n        // canceled or not — reported completion and the frame buffer is\r\n        // released. Clear the arm BEFORE deciding health, so the watchdog\r\n        // never severs while the op is formally in flight.\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = None;\r\n            inner.dog_cv.notify_all();\r\n        }\r\n\r\n        let mut g = recover(&inner.gate);\r\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\r\n            g.half = Some(half);\r\n            drop(g);\r\n            inner.gate_cv.notify_all();\r\n            return Ok(ConnWrite::Done);\r\n        }\r\n\r\n        // ── Retire: poison the whole conn, abort the read side so\r\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\r\n        // graveyard (its handle must outlive any late raw abort; never\r\n        // re-gated, never reused — a timed-out length-prefixed frame may be\r\n        // partially written). ──\r\n        drop(g);\r\n        inner.poison_and_cancel(res.as_ref().err());\r\n        inner.sever();\r\n        *recover(&inner.graveyard) = Some(half);\r\n        inner.gate_cv.notify_all();\r\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\r\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\r\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\r\n        // (peer death) keeps its own error.\r\n        if inner.timed_out.load(Ordering::Acquire) {\r\n            return Err(timeout_err());\r\n        }\r\n        Err(match res {\r\n            Err(e) => e,\r\n            Ok(()) => poisoned_err(),\r\n        })\r\n    }\r\n}\r\n\r\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\r\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\r\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\r\n/// window (the P1c epoch-gate keystone unit relies on it).\r\n#[cfg(test)]\r\npub(crate) struct TestGatePin {\r\n    inner: Arc<Inner>,\r\n    half: Option<SendHalf>,\r\n}\r\n\r\n#[cfg(test)]\r\nimpl Drop for TestGatePin {\r\n    fn drop(&mut self) {\r\n        let mut g = recover(&self.inner.gate);\r\n        g.half = self.half.take();\r\n        drop(g);\r\n        self.inner.gate_cv.notify_all();\r\n    }\r\n}\r\n\r","truncated":true,"truncatedBy":"lines","totalLines":407,"totalBytes":8564,"outputLines":215,"outputBytes":8564,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"impl Inner {\n…\n    fn watchdog(&self) {\n…\n    }\r\n}\r\n\r\n/// The shared, serialized, BOUNDED send side of one physical broker\r\n/// connection — what [`crate::broker::SharedSend`] now aliases. Owns the\r\n/// [`SendHalf`] (it never leaves this module) and the per-conn watchdog.\r\npub(crate) struct BrokerConn {\r\n    inner: Arc<Inner>,\r\n    /// Joined on drop — after `shutdown` is raised — so the raw handle the\r\n    /// watchdog aborts through strictly outlives every raw use.\r\n    dog: Option<JoinHandle<()>>,\r\n}\r\n\r\nimpl BrokerConn {\r\n    /// Wrap a freshly-split send half. `bound` is the ABSOLUTE per-write\r\n    /// deadline (gate-wait + OS write completion) — the broker passes its\r\n    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\r\n    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\r\n        let raw = raw_of(&half);\r\n        let inner = Arc::new(Inner {\r\n            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\r\n            label: Mutex::new(String::new()),\r\n            first_write_logged: AtomicBool::new(false),\r\n            gate: Mutex::new(Gate { half: Some(half) }),\r\n            gate_cv: Condvar::new(),\r\n            dog: Mutex::new(Dog {\r\n                inflight: None,\r\n                shutdown: false,\r\n            }),\r\n            dog_cv: Condvar::new(),\r\n            poisoned: AtomicBool::new(false),\r\n            timed_out: AtomicBool::new(false),\r\n            severed: AtomicBool::new(false),\r\n            graveyard: Mutex::new(None),\r\n            op_seq: AtomicU64::new(0),\r\n            bound,\r\n            raw,\r\n        });\r\n        let dog_inner = Arc::clone(&inner);\r\n        let dog = thread::Builder::new()\r\n            .name(\"conn-watchdog\".into())\r\n            .spawn(move || dog_inner.watchdog())\r\n            .ok();\r\n        BrokerConn { inner, dog }\r\n    }\r\n\r\n    /// The stable per-physical-conn id — what lifecycle records key on\r\n    /// (`Arc::ptr_eq` identity does not survive a log line).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn id(&self) -> u64 {\r\n        self.inner.id\r\n    }\r\n\r\n    /// Accrete an attribution fact onto this conn's label (subscriber role,\r\n    /// endpoint/session where known — e.g. `role=brain`,\r\n    /// `controller session=7 by=cli@host`). Facts accumulate space-separated\r\n    /// and ride EVERY subsequent lifecycle record for this conn; growth is\r\n    /// bounded by [`LABEL_CAP`] (one `…` marker, then no-op).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn describe(&self, fact: &str) {\r\n        let mut l = recover(&self.inner.label);\r\n        if l.ends_with('…') {\r\n            return;\r\n        }\r\n        if l.len() + fact.len() + 1 > LABEL_CAP {\r\n            l.push('…');\r\n            return;\r\n        }\r\n        if !l.is_empty() {\r\n            l.push(' ');\r\n        }\r\n        l.push_str(fact);\r\n    }\r\n\r\n    /// Emit a bounded per-conn lifecycle record through this conn's shared\r\n    /// attribution format — the broker's sink machinery uses it for\r\n    /// `writer-exit`, attach/replace, and detach events (leg d).\r\n    // [impl->REQ-CONN-POISON-ATTRIBUTION]\r\n    pub(crate) fn lifecycle_event(&self, event: &str, extra: &str) {\r\n        self.inner.lifecycle(event, extra);\r\n    }\r\n\r\n    /// Bounded framed write. See [`Self::write_gated`].\r\n    pub(crate) fn write(&self, env: &Envelope) -> io::Result<ConnWrite> {\r\n        self.write_gated(env, || true)\r\n    }\r\n\r\n    /// Bounded framed write with a pre-write gate closure, run AFTER the\r\n    /// serialized gate is acquired and BEFORE any byte is written — the same\r\n    /// serialization point the old epoch-check-under-`send.lock()` had, so a\r\n    /// superseded controller writer still bails atomically with respect to\r\n    /// its successor's writes (P1c, REQ-HAZARD-CONTROLLER-WRITER-REORDER).\r\n    /// `pre` returning false → `Ok(ConnWrite::Superseded)`, nothing written.\r\n    pub(crate) fn write_gated(\r\n        &self,\r\n        env: &Envelope,\r\n        pre: impl FnOnce() -> bool,\r\n    ) -> io::Result<ConnWrite> {\r\n        let inner = &*self.inner;\r\n        let deadline = Instant::now() + inner.bound;\r\n\r\n        // ── Acquire the gate, bounded by the SAME absolute deadline. ──\r\n        let mut g = recover(&inner.gate);\r\n        loop {\r\n            if inner.poisoned.load(Ordering::Acquire) {\r\n                return Err(poisoned_err());\r\n            }\r\n            if g.half.is_some() {\r\n                break;\r\n            }\r\n            let now = Instant::now();\r\n            if now >= deadline {\r\n                // The write we queued behind is stuck past OUR whole bound:\r\n                // that op is the wedge. Abort it out of band and fail — the\r\n                // watchdog's own deadline (armed at that op's entry) is also\r\n                // in flight; both paths converge on the same idempotent\r\n                // poison/cancel.\r\n                drop(g);\r\n                inner.timed_out.store(true, Ordering::Release);\r\n                inner.poison_and_cancel(None);\r\n                return Err(timeout_err());\r\n            }\r\n            let (g2, _) = inner\r\n                .gate_cv\r\n                .wait_timeout(g, deadline - now)\r\n                .unwrap_or_else(|p| p.into_inner());\r\n            g = g2;\r\n        }\r\n\r\n        if !pre() {\r\n            return Ok(ConnWrite::Superseded);\r\n        }\r\n\r\n        // Once-per-conn `write-start` lifecycle record (leg d): marks the conn's\r\n        // first committed write, so a conn that later retires can be correlated\r\n        // to when it went active (fresh-carrier churn shows as open→start→retire\r\n        // triplets with fresh ids).\r\n        if !inner.first_write_logged.swap(true, Ordering::AcqRel) {\r\n            inner.lifecycle(\"write-start\", \"\");\r\n        }\r\n\r\n        // ── Check out the half + arm the watchdog, then write OUT of locks. ──\r\n        let mut half = g.half.take().expect(\"gate half present at checkout\");\r\n        let op = inner.op_seq.fetch_add(1, Ordering::Relaxed) + 1;\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = Some((op, deadline));\r\n            inner.dog_cv.notify_all();\r\n        }\r\n        drop(g);\r\n\r\n        let res = write_frame(&mut half, env);\r\n\r\n        // Completion handshake: `write_frame` has returned, so the OS op —\r\n        // canceled or not — reported completion and the frame buffer is\r\n        // released. Clear the arm BEFORE deciding health, so the watchdog\r\n        // never severs while the op is formally in flight.\r\n        {\r\n            let mut d = recover(&inner.dog);\r\n            d.inflight = None;\r\n            inner.dog_cv.notify_all();\r\n        }\r\n\r\n        let mut g = recover(&inner.gate);\r\n        if res.is_ok() && !inner.poisoned.load(Ordering::Acquire) {\r\n            g.half = Some(half);\r\n            drop(g);\r\n            inner.gate_cv.notify_all();\r\n            return Ok(ConnWrite::Done);\r\n        }\r\n\r\n        // ── Retire: poison the whole conn, abort the read side so\r\n        // `handle_conn` reaches EOF cleanup, sever, and park the half in the\r\n        // graveyard (its handle must outlive any late raw abort; never\r\n        // re-gated, never reused — a timed-out length-prefixed frame may be\r\n        // partially written). ──\r\n        drop(g);\r\n        inner.poison_and_cancel(res.as_ref().err());\r\n        inner.sever();\r\n        *recover(&inner.graveyard) = Some(half);\r\n        inner.gate_cv.notify_all();\r\n        // A DEADLINE retirement (the watchdog aborted OUR op, or the conn was\r\n        // already deadline-poisoned) surfaces as TimedOut — the caller can\r\n        // count the wedge (W2 stall-evict tally). An organic I/O failure\r\n        // (peer death) keeps its own error.\r\n        if inner.timed_out.load(Ordering::Acquire) {\r\n            return Err(timeout_err());\r\n        }\r\n        Err(match res {\r\n            Err(e) => e,\r\n            Ok(()) => poisoned_err(),\r\n        })\r\n    }\r\n}\r\n\r\n/// TEST-ONLY determinism lever: holds the serialized write gate (the half is\r\n/// checked out) so every writer parks AT THE GATE until the pin drops — the\r\n/// round-4 analog of pinning the old `Mutex<SendHalf>` across a registration\r\n/// window (the P1c epoch-gate keystone unit relies on it).\r\n#[cfg(test)]\r\npub(crate) struct TestGatePin {\r\n    inner: Arc<Inner>,\r\n    half: Option<SendHalf>,\r\n}\r\n\r\n#[cfg(test)]\r\nimpl Drop for TestGatePin {\r\n    fn drop(&mut self) {\r\n        let mut g = recover(&self.inner.gate);\r\n        g.half = self.half.take();\r\n        drop(g);\r\n        self.inner.gate_cv.notify_all();\r\n    }\r\n}\r\n\r","startLine":219,"lineNumbers":[219,null,343,null,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623]},"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":8564,"outputLines":215,"outputBytes":8564,"shownRange":{"start":409,"end":623},"nextOffset":624}}},"isError":false,"timestamp":1784365568133}}
{"type":"message","id":"43ebc6a3","parentId":"e2542aa8","timestamp":"2026-07-18T09:06:08.133Z","message":{"role":"toolResult","toolCallId":"call_XsRamIatGYKdiFjvO6Mamr2n|fc_0ce5441d29b9f05d016a5b41fff7dc819bb364c12f4c165263","toolName":"read","content":[{"type":"text","text":"fn event_loop(terminal: &mut Term, model: &mut PickerModel) -> io::Result<Outcome> {\n    loop {\n…\n        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {\n…\n        }\n        // Any key clears a stale flash.\n        model.flash = None;\n\n        if let Some(outcome) = handle_key(model, key.code) {\n            match outcome {\n                Outcome::Shortcut {\n                    adapter,\n                    id,\n                    resume,\n                    mode,\n                } => {\n                    model.flash = Some(write_shortcut_now(&adapter, &id, resume.as_deref(), mode));\n                }\n                // B-2: apply the adapter change to the perch record, then STAY —\n                // update the in-memory row + return to Confirm (never exits the\n                // picker), mirroring the Shortcut side-effect handling.\n                Outcome::ChangeAdapter { id, adapter } => match write_adapter_change(&id, &adapter)\n                {\n                    Ok(()) => {\n                        model.apply_adapter_change(adapter.clone());\n                        model.flash = Some(format!(\"✓ harness adapter set: {adapter}\"));\n                    }\n                    Err(e) => {\n                        model.cancel_change_adapter();\n                        model.flash = Some(format!(\"! adapter change failed: {e}\"));\n                    }\n                },\n                // C-3: purge through the ONE existing purge core, then STAY —\n                // refresh the in-memory list + flash the verdict, back on the pick\n                // list either way. `yes=true` ALWAYS (the ConfirmPurge screen IS\n                // the confirm; the core's stdin [y/N] fights raw mode). `force=false`\n                // — the model gated on offline, and a race to online between gate\n                // and purge must REFUSE (the core's offline-only check is the\n                // authority), never stop-then-purge a live endpoint.\n                // [impl->REQ-PICKER-PURGE-SHORTCUT]\n                Outcome::Purge { id } => {\n                    match crate::cli::cmd_endpoint_purge(&id, true, false) {\n                        0 => {\n                            model.remove_endpoint(&id);\n                            model.flash = Some(format!(\"PURGED:{id}\"));\n                        }\n                        rc => model.flash = Some(format!(\"! purge failed (rc {rc})\")),\n                    }\n                    model.screen = Screen::PickExisting;\n                }\n                done => return Ok(done),\n            }\n        }\n    }\n}\n\n/// Translate one key into a model mutation and/or a yielded outcome. Pure\n/// dispatch over the model's transition methods — the model owns the semantics.\nfn handle_key(model: &mut PickerModel, code: KeyCode) -> Option<Outcome> {\n    // Esc backs out one screen (out of filter mode first); from the kind layer\n    // it cancels the whole picker.\n    if code == KeyCode::Esc {\n        if model.screen == Screen::PickExisting && model.filtering {\n            model.end_filter();\n            return None;\n        }\n        return model.back().then_some(Outcome::Cancelled);\n    }\n    // Backspace = back() alias (C-4) — EXCEPT in the two text-edit contexts\n    // (CreateId entry, the pick-list filter), where it stays char-delete: those\n    // fall through to their per-screen arms below. No empty-buffer fallthrough\n    // to back() — mixing delete and nav on one key in a text context invites\n    // miskeys; Esc already backs out. [impl->REQ-PICKER-BACK-NAV]\n    if code == KeyCode::Backspace\n        && model.screen != Screen::CreateId\n        && !(model.screen == Screen::PickExisting && model.filtering)\n    {\n        return model.back().then_some(Outcome::Cancelled);\n    }\n…\n}\n\n[Showing lines 129-203 of 606. Use :204 to continue]"}],"details":{"truncation":{"content":"        }\r\n        // Any key clears a stale flash.\r\n        model.flash = None;\r\n\r\n        if let Some(outcome) = handle_key(model, key.code) {\r\n            match outcome {\r\n                Outcome::Shortcut {\r\n                    adapter,\r\n                    id,\r\n                    resume,\r\n                    mode,\r\n                } => {\r\n                    model.flash = Some(write_shortcut_now(&adapter, &id, resume.as_deref(), mode));\r\n                }\r\n                // B-2: apply the adapter change to the perch record, then STAY —\r\n                // update the in-memory row + return to Confirm (never exits the\r\n                // picker), mirroring the Shortcut side-effect handling.\r\n                Outcome::ChangeAdapter { id, adapter } => match write_adapter_change(&id, &adapter)\r\n                {\r\n                    Ok(()) => {\r\n                        model.apply_adapter_change(adapter.clone());\r\n                        model.flash = Some(format!(\"✓ harness adapter set: {adapter}\"));\r\n                    }\r\n                    Err(e) => {\r\n                        model.cancel_change_adapter();\r\n                        model.flash = Some(format!(\"! adapter change failed: {e}\"));\r\n                    }\r\n                },\r\n                // C-3: purge through the ONE existing purge core, then STAY —\r\n                // refresh the in-memory list + flash the verdict, back on the pick\r\n                // list either way. `yes=true` ALWAYS (the ConfirmPurge screen IS\r\n                // the confirm; the core's stdin [y/N] fights raw mode). `force=false`\r\n                // — the model gated on offline, and a race to online between gate\r\n                // and purge must REFUSE (the core's offline-only check is the\r\n                // authority), never stop-then-purge a live endpoint.\r\n                // [impl->REQ-PICKER-PURGE-SHORTCUT]\r\n                Outcome::Purge { id } => {\r\n                    match crate::cli::cmd_endpoint_purge(&id, true, false) {\r\n                        0 => {\r\n                            model.remove_endpoint(&id);\r\n                            model.flash = Some(format!(\"PURGED:{id}\"));\r\n                        }\r\n                        rc => model.flash = Some(format!(\"! purge failed (rc {rc})\")),\r\n                    }\r\n                    model.screen = Screen::PickExisting;\r\n                }\r\n                done => return Ok(done),\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Translate one key into a model mutation and/or a yielded outcome. Pure\r\n/// dispatch over the model's transition methods — the model owns the semantics.\r\nfn handle_key(model: &mut PickerModel, code: KeyCode) -> Option<Outcome> {\r\n    // Esc backs out one screen (out of filter mode first); from the kind layer\r\n    // it cancels the whole picker.\r\n    if code == KeyCode::Esc {\r\n        if model.screen == Screen::PickExisting && model.filtering {\r\n            model.end_filter();\r\n            return None;\r\n        }\r\n        return model.back().then_some(Outcome::Cancelled);\r\n    }\r\n    // Backspace = back() alias (C-4) — EXCEPT in the two text-edit contexts\r\n    // (CreateId entry, the pick-list filter), where it stays char-delete: those\r\n    // fall through to their per-screen arms below. No empty-buffer fallthrough\r\n    // to back() — mixing delete and nav on one key in a text context invites\r\n    // miskeys; Esc already backs out. [impl->REQ-PICKER-BACK-NAV]\r\n    if code == KeyCode::Backspace\r\n        && model.screen != Screen::CreateId\r\n        && !(model.screen == Screen::PickExisting && model.filtering)\r\n    {\r\n        return model.back().then_some(Outcome::Cancelled);\r\n    }\r","truncated":true,"truncatedBy":"lines","totalLines":478,"totalBytes":3680,"outputLines":75,"outputBytes":3680,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn event_loop(terminal: &mut Term, model: &mut PickerModel) -> io::Result<Outcome> {\n    loop {\n…\n        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {\n…\n        }\r\n        // Any key clears a stale flash.\r\n        model.flash = None;\r\n\r\n        if let Some(outcome) = handle_key(model, key.code) {\r\n            match outcome {\r\n                Outcome::Shortcut {\r\n                    adapter,\r\n                    id,\r\n                    resume,\r\n                    mode,\r\n                } => {\r\n                    model.flash = Some(write_shortcut_now(&adapter, &id, resume.as_deref(), mode));\r\n                }\r\n                // B-2: apply the adapter change to the perch record, then STAY —\r\n                // update the in-memory row + return to Confirm (never exits the\r\n                // picker), mirroring the Shortcut side-effect handling.\r\n                Outcome::ChangeAdapter { id, adapter } => match write_adapter_change(&id, &adapter)\r\n                {\r\n                    Ok(()) => {\r\n                        model.apply_adapter_change(adapter.clone());\r\n                        model.flash = Some(format!(\"✓ harness adapter set: {adapter}\"));\r\n                    }\r\n                    Err(e) => {\r\n                        model.cancel_change_adapter();\r\n                        model.flash = Some(format!(\"! adapter change failed: {e}\"));\r\n                    }\r\n                },\r\n                // C-3: purge through the ONE existing purge core, then STAY —\r\n                // refresh the in-memory list + flash the verdict, back on the pick\r\n                // list either way. `yes=true` ALWAYS (the ConfirmPurge screen IS\r\n                // the confirm; the core's stdin [y/N] fights raw mode). `force=false`\r\n                // — the model gated on offline, and a race to online between gate\r\n                // and purge must REFUSE (the core's offline-only check is the\r\n                // authority), never stop-then-purge a live endpoint.\r\n                // [impl->REQ-PICKER-PURGE-SHORTCUT]\r\n                Outcome::Purge { id } => {\r\n                    match crate::cli::cmd_endpoint_purge(&id, true, false) {\r\n                        0 => {\r\n                            model.remove_endpoint(&id);\r\n                            model.flash = Some(format!(\"PURGED:{id}\"));\r\n                        }\r\n                        rc => model.flash = Some(format!(\"! purge failed (rc {rc})\")),\r\n                    }\r\n                    model.screen = Screen::PickExisting;\r\n                }\r\n                done => return Ok(done),\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Translate one key into a model mutation and/or a yielded outcome. Pure\r\n/// dispatch over the model's transition methods — the model owns the semantics.\r\nfn handle_key(model: &mut PickerModel, code: KeyCode) -> Option<Outcome> {\r\n    // Esc backs out one screen (out of filter mode first); from the kind layer\r\n    // it cancels the whole picker.\r\n    if code == KeyCode::Esc {\r\n        if model.screen == Screen::PickExisting && model.filtering {\r\n            model.end_filter();\r\n            return None;\r\n        }\r\n        return model.back().then_some(Outcome::Cancelled);\r\n    }\r\n    // Backspace = back() alias (C-4) — EXCEPT in the two text-edit contexts\r\n    // (CreateId entry, the pick-list filter), where it stays char-delete: those\r\n    // fall through to their per-screen arms below. No empty-buffer fallthrough\r\n    // to back() — mixing delete and nav on one key in a text context invites\r\n    // miskeys; Esc already backs out. [impl->REQ-PICKER-BACK-NAV]\r\n    if code == KeyCode::Backspace\r\n        && model.screen != Screen::CreateId\r\n        && !(model.screen == Screen::PickExisting && model.filtering)\r\n    {\r\n        return model.back().then_some(Outcome::Cancelled);\r\n    }\r\n…\n}","startLine":116,"lineNumbers":[116,117,null,127,null,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,null,281]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\picker\\mod.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":606,"totalBytes":3680,"outputLines":75,"outputBytes":3680,"shownRange":{"start":129,"end":203},"nextOffset":204}}},"isError":false,"timestamp":1784365568133}}
{"type":"message","id":"db76b4cb","parentId":"43ebc6a3","timestamp":"2026-07-18T09:06:08.140Z","message":{"role":"toolResult","toolCallId":"call_v1WOGGgXnpMo0veeCXioihIV|fc_0ce5441d29b9f05d016a5b41fff7b0819ba67ab81a369dc969","toolName":"read","content":[{"type":"text","text":"fn env_inject_test() {\n…\n    for _ in 0..200 {\n        match LocalSocketTransport::connect(&name) {\n…\n        }\n    }\n    let mut conn = conn.expect(\"connect to broker\");\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\n\n    // A child that prints the injected env var, cross-OS.\n    #[cfg(unix)]\n    let (program, args) = (\n        \"sh\".to_string(),\n        vec![\"-c\".to_string(), \"echo ENVCHECK=$SPT_ENDPOINT_ID\".to_string()],\n    );\n    #[cfg(windows)]\n    let (program, args) = (\n        \"cmd\".to_string(),\n        vec![\"/c\".to_string(), \"echo ENVCHECK=%SPT_ENDPOINT_ID%\".to_string()],\n    );\n    let mut env = std::collections::BTreeMap::new();\n    env.insert(\"SPT_ENDPOINT_ID\".to_string(), \"wall-b\".to_string());\n    send(\n        &mut conn,\n        KIND_SPAWN,\n        serde_json::to_value(SpawnReq {\n            program,\n            args,\n            rows: 24,\n            cols: 80,\n            endpoint: \"wall-b\".to_string(),\n            cwd: None,\n            env,\n            translation_binary: None,\n            adapter: String::new(),\n            install_dir: None,\n        })\n        .unwrap(),\n    );\n\n    // Bounded poll until the injected env echo appears. The child (`echo ENVCHECK=\n    // $SPT_ENDPOINT_ID` → exits) prints once then dies, so on a fast/quiet box the\n    // EXIT notification and a transient read Err can both arrive BEFORE the OUTPUT\n    // frame is delivered. The old loop broke on the first `Err` OR on `EXIT`, so it\n    // bailed with out=\"\" (a rotating contention victim, then an 18ms fast-fail on\n    // the serial Phase-B box). Instead RETRY on Err (every other read loop in this\n    // file does `Err => sleep(10ms)`), bound by a wall-clock deadline, and — once the\n    // child has EXITed — keep draining briefly so a trailing OUTPUT frame still lands\n    // before we give up.\n    let mut out = Vec::<u8>::new();\n    let mut got = false;\n    let mut exited = false;\n    let mut post_exit_drains = 0;\n    let deadline = std::time::Instant::now() + Duration::from_secs(10);\n    while std::time::Instant::now() < deadline {\n        if String::from_utf8_lossy(&out).contains(\"ENVCHECK=wall-b\") {\n            got = true;\n            break;\n        }\n        match read_frame(&mut conn) {\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\n            Ok(f) if f.kind == KIND_EXIT => exited = true,\n            Ok(_) => {}\n            Err(_) => {\n                // After EXIT a run of read errors means the stream is fully drained\n                // (no trailing output is coming) — stop. Before EXIT the frame just\n                // isn't here yet, so wait and retry.\n                if exited {\n                    post_exit_drains += 1;\n                    if post_exit_drains >= 5 {\n                        break;\n                    }\n                }\n                thread::sleep(Duration::from_millis(10));\n            }\n        }\n    }\n    drop(conn);\n    server.join().expect(\"server thread\");\n    assert!(\n        got,\n        \"injected SpawnReq.env must reach the child: {:?}\",\n        String::from_utf8_lossy(&out)\n    );\n}\n\n#[test]\nfn spawn_env_reaches_child() {\n    env_inject_test();\n}\n\n/// REQ-HAZARD-RC-ATTACH-FAILFAST path (c): a subscribe to a session whose child\n/// has EXITED surfaces a prompt Exit (dead-not-reaped → dispatch_subscribe's\n/// try_wait → Exit frame) or a clean \"no such session\" error (already reaped) —\n\n[Showing lines 339-428 of 1221. Use :429 to continue]"}],"details":{"truncation":{"content":"        }\r\n    }\r\n    let mut conn = conn.expect(\"connect to broker\");\r\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\r\n\r\n    // A child that prints the injected env var, cross-OS.\r\n    #[cfg(unix)]\r\n    let (program, args) = (\r\n        \"sh\".to_string(),\r\n        vec![\"-c\".to_string(), \"echo ENVCHECK=$SPT_ENDPOINT_ID\".to_string()],\r\n    );\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"cmd\".to_string(),\r\n        vec![\"/c\".to_string(), \"echo ENVCHECK=%SPT_ENDPOINT_ID%\".to_string()],\r\n    );\r\n    let mut env = std::collections::BTreeMap::new();\r\n    env.insert(\"SPT_ENDPOINT_ID\".to_string(), \"wall-b\".to_string());\r\n    send(\r\n        &mut conn,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(SpawnReq {\r\n            program,\r\n            args,\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"wall-b\".to_string(),\r\n            cwd: None,\r\n            env,\r\n            translation_binary: None,\r\n            adapter: String::new(),\r\n            install_dir: None,\r\n        })\r\n        .unwrap(),\r\n    );\r\n\r\n    // Bounded poll until the injected env echo appears. The child (`echo ENVCHECK=\r\n    // $SPT_ENDPOINT_ID` → exits) prints once then dies, so on a fast/quiet box the\r\n    // EXIT notification and a transient read Err can both arrive BEFORE the OUTPUT\r\n    // frame is delivered. The old loop broke on the first `Err` OR on `EXIT`, so it\r\n    // bailed with out=\"\" (a rotating contention victim, then an 18ms fast-fail on\r\n    // the serial Phase-B box). Instead RETRY on Err (every other read loop in this\r\n    // file does `Err => sleep(10ms)`), bound by a wall-clock deadline, and — once the\r\n    // child has EXITed — keep draining briefly so a trailing OUTPUT frame still lands\r\n    // before we give up.\r\n    let mut out = Vec::<u8>::new();\r\n    let mut got = false;\r\n    let mut exited = false;\r\n    let mut post_exit_drains = 0;\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(10);\r\n    while std::time::Instant::now() < deadline {\r\n        if String::from_utf8_lossy(&out).contains(\"ENVCHECK=wall-b\") {\r\n            got = true;\r\n            break;\r\n        }\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(f) if f.kind == KIND_EXIT => exited = true,\r\n            Ok(_) => {}\r\n            Err(_) => {\r\n                // After EXIT a run of read errors means the stream is fully drained\r\n                // (no trailing output is coming) — stop. Before EXIT the frame just\r\n                // isn't here yet, so wait and retry.\r\n                if exited {\r\n                    post_exit_drains += 1;\r\n                    if post_exit_drains >= 5 {\r\n                        break;\r\n                    }\r\n                }\r\n                thread::sleep(Duration::from_millis(10));\r\n            }\r\n        }\r\n    }\r\n    drop(conn);\r\n    server.join().expect(\"server thread\");\r\n    assert!(\r\n        got,\r\n        \"injected SpawnReq.env must reach the child: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n}\r\n\r\n#[test]\r\nfn spawn_env_reaches_child() {\r\n    env_inject_test();\r\n}\r\n\r\n/// REQ-HAZARD-RC-ATTACH-FAILFAST path (c): a subscribe to a session whose child\r\n/// has EXITED surfaces a prompt Exit (dead-not-reaped → dispatch_subscribe's\r\n/// try_wait → Exit frame) or a clean \"no such session\" error (already reaped) —\r","truncated":true,"truncatedBy":"lines","totalLines":883,"totalBytes":3426,"outputLines":90,"outputBytes":3426,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn env_inject_test() {\n…\n    for _ in 0..200 {\n        match LocalSocketTransport::connect(&name) {\n…\n        }\r\n    }\r\n    let mut conn = conn.expect(\"connect to broker\");\r\n    send_hello(&mut conn, Role::Brain).expect(\"send hello\");\r\n\r\n    // A child that prints the injected env var, cross-OS.\r\n    #[cfg(unix)]\r\n    let (program, args) = (\r\n        \"sh\".to_string(),\r\n        vec![\"-c\".to_string(), \"echo ENVCHECK=$SPT_ENDPOINT_ID\".to_string()],\r\n    );\r\n    #[cfg(windows)]\r\n    let (program, args) = (\r\n        \"cmd\".to_string(),\r\n        vec![\"/c\".to_string(), \"echo ENVCHECK=%SPT_ENDPOINT_ID%\".to_string()],\r\n    );\r\n    let mut env = std::collections::BTreeMap::new();\r\n    env.insert(\"SPT_ENDPOINT_ID\".to_string(), \"wall-b\".to_string());\r\n    send(\r\n        &mut conn,\r\n        KIND_SPAWN,\r\n        serde_json::to_value(SpawnReq {\r\n            program,\r\n            args,\r\n            rows: 24,\r\n            cols: 80,\r\n            endpoint: \"wall-b\".to_string(),\r\n            cwd: None,\r\n            env,\r\n            translation_binary: None,\r\n            adapter: String::new(),\r\n            install_dir: None,\r\n        })\r\n        .unwrap(),\r\n    );\r\n\r\n    // Bounded poll until the injected env echo appears. The child (`echo ENVCHECK=\r\n    // $SPT_ENDPOINT_ID` → exits) prints once then dies, so on a fast/quiet box the\r\n    // EXIT notification and a transient read Err can both arrive BEFORE the OUTPUT\r\n    // frame is delivered. The old loop broke on the first `Err` OR on `EXIT`, so it\r\n    // bailed with out=\"\" (a rotating contention victim, then an 18ms fast-fail on\r\n    // the serial Phase-B box). Instead RETRY on Err (every other read loop in this\r\n    // file does `Err => sleep(10ms)`), bound by a wall-clock deadline, and — once the\r\n    // child has EXITed — keep draining briefly so a trailing OUTPUT frame still lands\r\n    // before we give up.\r\n    let mut out = Vec::<u8>::new();\r\n    let mut got = false;\r\n    let mut exited = false;\r\n    let mut post_exit_drains = 0;\r\n    let deadline = std::time::Instant::now() + Duration::from_secs(10);\r\n    while std::time::Instant::now() < deadline {\r\n        if String::from_utf8_lossy(&out).contains(\"ENVCHECK=wall-b\") {\r\n            got = true;\r\n            break;\r\n        }\r\n        match read_frame(&mut conn) {\r\n            Ok(f) if f.kind == KIND_OUTPUT => accumulate_output(&f, &mut out),\r\n            Ok(f) if f.kind == KIND_EXIT => exited = true,\r\n            Ok(_) => {}\r\n            Err(_) => {\r\n                // After EXIT a run of read errors means the stream is fully drained\r\n                // (no trailing output is coming) — stop. Before EXIT the frame just\r\n                // isn't here yet, so wait and retry.\r\n                if exited {\r\n                    post_exit_drains += 1;\r\n                    if post_exit_drains >= 5 {\r\n                        break;\r\n                    }\r\n                }\r\n                thread::sleep(Duration::from_millis(10));\r\n            }\r\n        }\r\n    }\r\n    drop(conn);\r\n    server.join().expect(\"server thread\");\r\n    assert!(\r\n        got,\r\n        \"injected SpawnReq.env must reach the child: {:?}\",\r\n        String::from_utf8_lossy(&out)\r\n    );\r\n}\r\n\r\n#[test]\r\nfn spawn_env_reaches_child() {\r\n    env_inject_test();\r\n}\r\n\r\n/// REQ-HAZARD-RC-ATTACH-FAILFAST path (c): a subscribe to a session whose child\r\n/// has EXITED surfaces a prompt Exit (dead-not-reaped → dispatch_subscribe's\r\n/// try_wait → Exit frame) or a clean \"no such session\" error (already reaped) —\r","startLine":323,"lineNumbers":[323,null,332,333,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\tests\\broker.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1221,"totalBytes":3426,"outputLines":90,"outputBytes":3426,"shownRange":{"start":339,"end":428},"nextOffset":429}}},"isError":false,"timestamp":1784365568140}}
{"type":"message","id":"97748b0d","parentId":"db76b4cb","timestamp":"2026-07-18T09:06:08.255Z","message":{"role":"toolResult","toolCallId":"call_ZjS5njP2FOoNqsGHhe1M2Qxc|fc_0ce5441d29b9f05d016a5b41fff7e0819bb93b712687a5dc9e","toolName":"read","content":[{"type":"text","text":"/// Refuses the caller's OWN running id. Confirms interactively unless `--yes`. Local\n/// only: a remote endpoint's records can't be reached — its subnet rows decay by lease\n/// (REQ-HAZARD-REGISTRY-DECAY). Generalizes `fork --delete-source` (recursive perch\n/// remove + unregister + context remove_endpoint) plus the trust-row cleanup, behind\n/// `rename`'s offline-only gate.\n// [impl->REQ-ENDPOINT-PURGE]\n// pub(crate): the picker's `x` purge shortcut routes through THIS core (the one\n// purge path — no second purge impl; REQ-PICKER-PURGE-SHORTCUT).\npub(crate) fn cmd_endpoint_purge(id: &str, yes: bool, force: bool) -> i32 {\n    let owlery = perch::owlery_dir();\n    // Self-guard: never purge the endpoint THIS session is running as (the harness\n    // injects SPT_ENDPOINT_ID = the id) — that would delete your own records mid-run.\n    if std::env::var(\"SPT_ENDPOINT_ID\").ok().as_deref() == Some(id) {\n        eprintln!(\"ERROR: refusing to purge '{id}' — it is THIS session's own running endpoint\");\n        return 2;\n    }\n    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n\n    // Offline-only gate: a live / daemon-hosted endpoint is refused unless --force,\n    // which stops it then waits for the daemon reconcile to un-host + reap the Psyche.\n    if spt_store::liveness::is_perch_alive(&perch) {\n        if !force {\n            eprintln!(\n                \"ERROR: '{id}' is online — purge is offline-only. Stop it first \\\n                 (`spt endpoint stop {id}`) or pass --force to stop-then-purge.\"\n            );\n            return 1;\n        }\n        eprintln!(\"--force: stopping '{id}' before purge ...\");\n        let _ = cmd_stop(id); // stamps the perch offline + unregisters the address\n        // Wait (bounded) until offline AND the {id}-psyche process is reaped, so we\n        // never delete records out from under a daemon still tearing the host down.\n        let psyche_perch =\n            perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\n        let psyche_alive = || {\n            spt_store::info::read_pid(&psyche_perch)\n                .map(spt_store::proc::is_process_alive)\n                .unwrap_or(false)\n        };\n        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);\n        while std::time::Instant::now() < deadline {\n            if !spt_store::liveness::is_perch_alive(&perch) && !psyche_alive() {\n                break;\n            }\n            std::thread::sleep(std::time::Duration::from_millis(200));\n        }\n        if spt_store::liveness::is_perch_alive(&perch) || psyche_alive() {\n            eprintln!(\"WARN: '{id}' did not fully quiesce within the stop window — purging anyway\");\n        }\n    }\n\n    // Confirm (destructive + irreversible) unless --yes (the CI path).\n    if !yes {\n        eprint!(\n            \"PURGE '{id}' and ALL its records (perch tree, context branches, registry + trust \\\n             rows)? This CANNOT be undone. [y/N]: \"\n        );\n        let _ = std::io::Write::flush(&mut std::io::stderr());\n        let mut line = String::new();\n        if std::io::stdin().read_line(&mut line).is_err()\n            || !matches!(line.trim(), \"y\" | \"Y\" | \"yes\" | \"YES\")\n        {\n            eprintln!(\"aborted — nothing purged\");\n            return 1;\n        }\n    }\n\n    // Remove every node-local record (best-effort: keep going so a partial state is\n    // still cleaned; a non-zero exit is reserved for the perch tree surviving).\n    // (1) context store: the a-<id> branch+worktree + the <id>/ rows in every\n    //     p-<project> branch (the fn `fork --delete-source` uses).\n    if let Ok(cs) = spt_store::contextstore::ContextStore::open_or_init() {\n        if let Err(e) = cs.remove_endpoint(id) {\n            eprintln!(\"WARN: context remove failed for '{id}': {e}\");\n        }\n    }\n    // (2) ready marker + registry address.\n    let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n    let _ = registry::unregister_address(id, &owlery);\n    // (3) node-local trust rows keyed on the id.\n    {\n        let mut access = spt_store::access::AccessStore::load();\n        if access.open(id) {\n            let _ = access.save();\n        }\n    }\n    {\n        let mut vis = spt_store::visibility::VisibilityStore::load();\n        if vis.remove_endpoint(id) {\n            let _ = vis.save();\n        }\n    }\n    // (4) the perch dir TREE — recursive, so the nested {id}-psyche / {id}-w* children\n    //     and the shells/ namespace under owlery/<id>/ go with it.\n    if perch.exists() {\n        if let Err(e) = std::fs::remove_dir_all(&perch) {\n            eprintln!(\"ERROR: failed to remove the perch tree for '{id}': {e}\");\n            return 1;\n        }\n    }\n    // Purge → drop the endpoint's project-index row (the roster is the\n    // authority; the writer removes a row whose perch is gone) — ADR-0037 §3.\n    // The context-store arm above already fired its own Global nudge.\n    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n        id: id.to_string(),\n        cwd: None,\n    });\n    eprintln!(\"PURGED:{id}\");\n    0\n}\n\nfn now_unix() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_secs())\n        .unwrap_or(0)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // [unit->REQ-MSG-CLI-ORIGIN] a from-less bare CLI send stamps cli@<node-label>;\n    // with no label known it degrades to bare `cli` — NEVER a dangling `cli@`.\n    #[test]\n    fn cli_send_stamps_cli_at_node_origin() {\n        assert_eq!(cli_origin_from_label(Some(\"ENLYZEAM\")), \"cli@ENLYZEAM\");\n        // No label / empty label → bare `cli`, no trailing separator.\n        assert_eq!(cli_origin_from_label(None), \"cli\");\n        assert_eq!(cli_origin_from_label(Some(\"\")), \"cli\");\n        assert!(!cli_origin_from_label(None).ends_with('@'), \"never a dangling cli@\");\n        // The live resolver is never blank and never a dangling @ (label or bare cli).\n        let live = cli_origin_label();\n        assert!(!live.is_empty() && !live.ends_with('@'));\n        assert!(live == \"cli\" || live.starts_with(\"cli@\"));\n    }\n\n    fn parse(args: &[&str]) -> Result<Cli, clap::Error> {\n        Cli::try_parse_from(args)\n    }\n…\n}\n\n[Showing lines 12699-12838 of 19200. Use :12839 to continue]"}],"details":{"truncation":{"content":"/// Refuses the caller's OWN running id. Confirms interactively unless `--yes`. Local\r\n/// only: a remote endpoint's records can't be reached — its subnet rows decay by lease\r\n/// (REQ-HAZARD-REGISTRY-DECAY). Generalizes `fork --delete-source` (recursive perch\r\n/// remove + unregister + context remove_endpoint) plus the trust-row cleanup, behind\r\n/// `rename`'s offline-only gate.\r\n// [impl->REQ-ENDPOINT-PURGE]\r\n// pub(crate): the picker's `x` purge shortcut routes through THIS core (the one\r\n// purge path — no second purge impl; REQ-PICKER-PURGE-SHORTCUT).\r\npub(crate) fn cmd_endpoint_purge(id: &str, yes: bool, force: bool) -> i32 {\r\n    let owlery = perch::owlery_dir();\r\n    // Self-guard: never purge the endpoint THIS session is running as (the harness\r\n    // injects SPT_ENDPOINT_ID = the id) — that would delete your own records mid-run.\r\n    if std::env::var(\"SPT_ENDPOINT_ID\").ok().as_deref() == Some(id) {\r\n        eprintln!(\"ERROR: refusing to purge '{id}' — it is THIS session's own running endpoint\");\r\n        return 2;\r\n    }\r\n    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\r\n\r\n    // Offline-only gate: a live / daemon-hosted endpoint is refused unless --force,\r\n    // which stops it then waits for the daemon reconcile to un-host + reap the Psyche.\r\n    if spt_store::liveness::is_perch_alive(&perch) {\r\n        if !force {\r\n            eprintln!(\r\n                \"ERROR: '{id}' is online — purge is offline-only. Stop it first \\\r\n                 (`spt endpoint stop {id}`) or pass --force to stop-then-purge.\"\r\n            );\r\n            return 1;\r\n        }\r\n        eprintln!(\"--force: stopping '{id}' before purge ...\");\r\n        let _ = cmd_stop(id); // stamps the perch offline + unregisters the address\r\n        // Wait (bounded) until offline AND the {id}-psyche process is reaped, so we\r\n        // never delete records out from under a daemon still tearing the host down.\r\n        let psyche_perch =\r\n            perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\r\n        let psyche_alive = || {\r\n            spt_store::info::read_pid(&psyche_perch)\r\n                .map(spt_store::proc::is_process_alive)\r\n                .unwrap_or(false)\r\n        };\r\n        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);\r\n        while std::time::Instant::now() < deadline {\r\n            if !spt_store::liveness::is_perch_alive(&perch) && !psyche_alive() {\r\n                break;\r\n            }\r\n            std::thread::sleep(std::time::Duration::from_millis(200));\r\n        }\r\n        if spt_store::liveness::is_perch_alive(&perch) || psyche_alive() {\r\n            eprintln!(\"WARN: '{id}' did not fully quiesce within the stop window — purging anyway\");\r\n        }\r\n    }\r\n\r\n    // Confirm (destructive + irreversible) unless --yes (the CI path).\r\n    if !yes {\r\n        eprint!(\r\n            \"PURGE '{id}' and ALL its records (perch tree, context branches, registry + trust \\\r\n             rows)? This CANNOT be undone. [y/N]: \"\r\n        );\r\n        let _ = std::io::Write::flush(&mut std::io::stderr());\r\n        let mut line = String::new();\r\n        if std::io::stdin().read_line(&mut line).is_err()\r\n            || !matches!(line.trim(), \"y\" | \"Y\" | \"yes\" | \"YES\")\r\n        {\r\n            eprintln!(\"aborted — nothing purged\");\r\n            return 1;\r\n        }\r\n    }\r\n\r\n    // Remove every node-local record (best-effort: keep going so a partial state is\r\n    // still cleaned; a non-zero exit is reserved for the perch tree surviving).\r\n    // (1) context store: the a-<id> branch+worktree + the <id>/ rows in every\r\n    //     p-<project> branch (the fn `fork --delete-source` uses).\r\n    if let Ok(cs) = spt_store::contextstore::ContextStore::open_or_init() {\r\n        if let Err(e) = cs.remove_endpoint(id) {\r\n            eprintln!(\"WARN: context remove failed for '{id}': {e}\");\r\n        }\r\n    }\r\n    // (2) ready marker + registry address.\r\n    let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\r\n    let _ = registry::unregister_address(id, &owlery);\r\n    // (3) node-local trust rows keyed on the id.\r\n    {\r\n        let mut access = spt_store::access::AccessStore::load();\r\n        if access.open(id) {\r\n            let _ = access.save();\r\n        }\r\n    }\r\n    {\r\n        let mut vis = spt_store::visibility::VisibilityStore::load();\r\n        if vis.remove_endpoint(id) {\r\n            let _ = vis.save();\r\n        }\r\n    }\r\n    // (4) the perch dir TREE — recursive, so the nested {id}-psyche / {id}-w* children\r\n    //     and the shells/ namespace under owlery/<id>/ go with it.\r\n    if perch.exists() {\r\n        if let Err(e) = std::fs::remove_dir_all(&perch) {\r\n            eprintln!(\"ERROR: failed to remove the perch tree for '{id}': {e}\");\r\n            return 1;\r\n        }\r\n    }\r\n    // Purge → drop the endpoint's project-index row (the roster is the\r\n    // authority; the writer removes a row whose perch is gone) — ADR-0037 §3.\r\n    // The context-store arm above already fired its own Global nudge.\r\n    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\r\n        id: id.to_string(),\r\n        cwd: None,\r\n    });\r\n    eprintln!(\"PURGED:{id}\");\r\n    0\r\n}\r\n\r\nfn now_unix() -> u64 {\r\n    std::time::SystemTime::now()\r\n        .duration_since(std::time::UNIX_EPOCH)\r\n        .map(|d| d.as_secs())\r\n        .unwrap_or(0)\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    // [unit->REQ-MSG-CLI-ORIGIN] a from-less bare CLI send stamps cli@<node-label>;\r\n    // with no label known it degrades to bare `cli` — NEVER a dangling `cli@`.\r\n    #[test]\r\n    fn cli_send_stamps_cli_at_node_origin() {\r\n        assert_eq!(cli_origin_from_label(Some(\"ENLYZEAM\")), \"cli@ENLYZEAM\");\r\n        // No label / empty label → bare `cli`, no trailing separator.\r\n        assert_eq!(cli_origin_from_label(None), \"cli\");\r\n        assert_eq!(cli_origin_from_label(Some(\"\")), \"cli\");\r\n        assert!(!cli_origin_from_label(None).ends_with('@'), \"never a dangling cli@\");\r\n        // The live resolver is never blank and never a dangling @ (label or bare cli).\r\n        let live = cli_origin_label();\r\n        assert!(!live.is_empty() && !live.ends_with('@'));\r\n        assert!(live == \"cli\" || live.starts_with(\"cli@\"));\r\n    }\r\n\r\n    fn parse(args: &[&str]) -> Result<Cli, clap::Error> {\r\n        Cli::try_parse_from(args)\r","truncated":true,"truncatedBy":"lines","totalLines":6502,"totalBytes":6469,"outputLines":140,"outputBytes":6469,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"/// Refuses the caller's OWN running id. Confirms interactively unless `--yes`. Local\r\n/// only: a remote endpoint's records can't be reached — its subnet rows decay by lease\r\n/// (REQ-HAZARD-REGISTRY-DECAY). Generalizes `fork --delete-source` (recursive perch\r\n/// remove + unregister + context remove_endpoint) plus the trust-row cleanup, behind\r\n/// `rename`'s offline-only gate.\r\n// [impl->REQ-ENDPOINT-PURGE]\r\n// pub(crate): the picker's `x` purge shortcut routes through THIS core (the one\r\n// purge path — no second purge impl; REQ-PICKER-PURGE-SHORTCUT).\r\npub(crate) fn cmd_endpoint_purge(id: &str, yes: bool, force: bool) -> i32 {\r\n    let owlery = perch::owlery_dir();\r\n    // Self-guard: never purge the endpoint THIS session is running as (the harness\r\n    // injects SPT_ENDPOINT_ID = the id) — that would delete your own records mid-run.\r\n    if std::env::var(\"SPT_ENDPOINT_ID\").ok().as_deref() == Some(id) {\r\n        eprintln!(\"ERROR: refusing to purge '{id}' — it is THIS session's own running endpoint\");\r\n        return 2;\r\n    }\r\n    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\r\n\r\n    // Offline-only gate: a live / daemon-hosted endpoint is refused unless --force,\r\n    // which stops it then waits for the daemon reconcile to un-host + reap the Psyche.\r\n    if spt_store::liveness::is_perch_alive(&perch) {\r\n        if !force {\r\n            eprintln!(\r\n                \"ERROR: '{id}' is online — purge is offline-only. Stop it first \\\r\n                 (`spt endpoint stop {id}`) or pass --force to stop-then-purge.\"\r\n            );\r\n            return 1;\r\n        }\r\n        eprintln!(\"--force: stopping '{id}' before purge ...\");\r\n        let _ = cmd_stop(id); // stamps the perch offline + unregisters the address\r\n        // Wait (bounded) until offline AND the {id}-psyche process is reaped, so we\r\n        // never delete records out from under a daemon still tearing the host down.\r\n        let psyche_perch =\r\n            perch::resolve_perch_path(&format!(\"{id}-psyche\"), ParentHint::Explicit(id));\r\n        let psyche_alive = || {\r\n            spt_store::info::read_pid(&psyche_perch)\r\n                .map(spt_store::proc::is_process_alive)\r\n                .unwrap_or(false)\r\n        };\r\n        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);\r\n        while std::time::Instant::now() < deadline {\r\n            if !spt_store::liveness::is_perch_alive(&perch) && !psyche_alive() {\r\n                break;\r\n            }\r\n            std::thread::sleep(std::time::Duration::from_millis(200));\r\n        }\r\n        if spt_store::liveness::is_perch_alive(&perch) || psyche_alive() {\r\n            eprintln!(\"WARN: '{id}' did not fully quiesce within the stop window — purging anyway\");\r\n        }\r\n    }\r\n\r\n    // Confirm (destructive + irreversible) unless --yes (the CI path).\r\n    if !yes {\r\n        eprint!(\r\n            \"PURGE '{id}' and ALL its records (perch tree, context branches, registry + trust \\\r\n             rows)? This CANNOT be undone. [y/N]: \"\r\n        );\r\n        let _ = std::io::Write::flush(&mut std::io::stderr());\r\n        let mut line = String::new();\r\n        if std::io::stdin().read_line(&mut line).is_err()\r\n            || !matches!(line.trim(), \"y\" | \"Y\" | \"yes\" | \"YES\")\r\n        {\r\n            eprintln!(\"aborted — nothing purged\");\r\n            return 1;\r\n        }\r\n    }\r\n\r\n    // Remove every node-local record (best-effort: keep going so a partial state is\r\n    // still cleaned; a non-zero exit is reserved for the perch tree surviving).\r\n    // (1) context store: the a-<id> branch+worktree + the <id>/ rows in every\r\n    //     p-<project> branch (the fn `fork --delete-source` uses).\r\n    if let Ok(cs) = spt_store::contextstore::ContextStore::open_or_init() {\r\n        if let Err(e) = cs.remove_endpoint(id) {\r\n            eprintln!(\"WARN: context remove failed for '{id}': {e}\");\r\n        }\r\n    }\r\n    // (2) ready marker + registry address.\r\n    let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\r\n    let _ = registry::unregister_address(id, &owlery);\r\n    // (3) node-local trust rows keyed on the id.\r\n    {\r\n        let mut access = spt_store::access::AccessStore::load();\r\n        if access.open(id) {\r\n            let _ = access.save();\r\n        }\r\n    }\r\n    {\r\n        let mut vis = spt_store::visibility::VisibilityStore::load();\r\n        if vis.remove_endpoint(id) {\r\n            let _ = vis.save();\r\n        }\r\n    }\r\n    // (4) the perch dir TREE — recursive, so the nested {id}-psyche / {id}-w* children\r\n    //     and the shells/ namespace under owlery/<id>/ go with it.\r\n    if perch.exists() {\r\n        if let Err(e) = std::fs::remove_dir_all(&perch) {\r\n            eprintln!(\"ERROR: failed to remove the perch tree for '{id}': {e}\");\r\n            return 1;\r\n        }\r\n    }\r\n    // Purge → drop the endpoint's project-index row (the roster is the\r\n    // authority; the writer removes a row whose perch is gone) — ADR-0037 §3.\r\n    // The context-store arm above already fired its own Global nudge.\r\n    // [impl->REQ-PROJECT-INDEX-INVALIDATION]\r\n    spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\r\n        id: id.to_string(),\r\n        cwd: None,\r\n    });\r\n    eprintln!(\"PURGED:{id}\");\r\n    0\r\n}\r\n\r\nfn now_unix() -> u64 {\r\n    std::time::SystemTime::now()\r\n        .duration_since(std::time::UNIX_EPOCH)\r\n        .map(|d| d.as_secs())\r\n        .unwrap_or(0)\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    // [unit->REQ-MSG-CLI-ORIGIN] a from-less bare CLI send stamps cli@<node-label>;\r\n    // with no label known it degrades to bare `cli` — NEVER a dangling `cli@`.\r\n    #[test]\r\n    fn cli_send_stamps_cli_at_node_origin() {\r\n        assert_eq!(cli_origin_from_label(Some(\"ENLYZEAM\")), \"cli@ENLYZEAM\");\r\n        // No label / empty label → bare `cli`, no trailing separator.\r\n        assert_eq!(cli_origin_from_label(None), \"cli\");\r\n        assert_eq!(cli_origin_from_label(Some(\"\")), \"cli\");\r\n        assert!(!cli_origin_from_label(None).ends_with('@'), \"never a dangling cli@\");\r\n        // The live resolver is never blank and never a dangling @ (label or bare cli).\r\n        let live = cli_origin_label();\r\n        assert!(!live.is_empty() && !live.ends_with('@'));\r\n        assert!(live == \"cli\" || live.starts_with(\"cli@\"));\r\n    }\r\n\r\n    fn parse(args: &[&str]) -> Result<Cli, clap::Error> {\r\n        Cli::try_parse_from(args)\r\n    }\n…\n}","startLine":12699,"lineNumbers":[12699,12700,12701,12702,12703,12704,12705,12706,12707,12708,12709,12710,12711,12712,12713,12714,12715,12716,12717,12718,12719,12720,12721,12722,12723,12724,12725,12726,12727,12728,12729,12730,12731,12732,12733,12734,12735,12736,12737,12738,12739,12740,12741,12742,12743,12744,12745,12746,12747,12748,12749,12750,12751,12752,12753,12754,12755,12756,12757,12758,12759,12760,12761,12762,12763,12764,12765,12766,12767,12768,12769,12770,12771,12772,12773,12774,12775,12776,12777,12778,12779,12780,12781,12782,12783,12784,12785,12786,12787,12788,12789,12790,12791,12792,12793,12794,12795,12796,12797,12798,12799,12800,12801,12802,12803,12804,12805,12806,12807,12808,12809,12810,12811,12812,12813,12814,12815,12816,12817,12818,12819,12820,12821,12822,12823,12824,12825,12826,12827,12828,12829,12830,12831,12832,12833,12834,12835,12836,12837,12838,12839,null,19199]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":19200,"totalBytes":6469,"outputLines":140,"outputBytes":6469,"shownRange":{"start":12699,"end":12838},"nextOffset":12839}}},"isError":false,"timestamp":1784365568254}}
{"type":"message","id":"66ffbe26","parentId":"97748b0d","timestamp":"2026-07-18T09:06:21.569Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Planning byte-level output reproduction**\n\n**Designing exit coordination barrier**"},{"type":"toolCall","id":"call_lJDvQlY7floC6VtWSYstap7v|fc_0ce5441d29b9f05d016a5b420d7a78819bbf1742b042bacc65","name":"grep","arguments":{"i":"Locating sink fanout helper","pattern":"fn all_sinks|all_sinks\\(","path":"crates/spt-daemon/src/broker.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_pxpPuRZKFhIdlCApaQ9TyxLX|fc_0ce5441d29b9f05d016a5b420d7a84819b954d6f92b486d977","name":"read","arguments":{"i":"Reading sink helper","path":"crates/spt-daemon/src/broker.rs","selector":"1420-1485,1750-1795"}},{"type":"toolCall","id":"call_Fqxk5SS5RylmgTuOcuAKMjl2|fc_0ce5441d29b9f05d016a5b420d7a8c819bbb405e21001a8bad","name":"grep","arguments":{"i":"Finding terminal setup function","pattern":"setup_terminal|restore_terminal|EnterAlternateScreen|LeaveAlternateScreen|terminal.clear","path":"crates/spt/src/picker/mod.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_4SphK8TzuyIkhgGIIzjP3Wiu|fc_0ce5441d29b9f05d016a5b420d7a94819b88bdfe27ae27f759","name":"read","arguments":{"i":"Reading picker terminal lifecycle","path":"crates/spt/src/picker/mod.rs","selector":"35-130"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":10895,"output":439,"cacheRead":197120,"cacheWrite":0,"totalTokens":208454,"reasoningTokens":215,"cost":{"input":0.054475,"output":0.013170000000000001,"cacheRead":0.09856,"cacheWrite":0,"total":0.166205}},"stopReason":"toolUse","timestamp":1784365568301,"responseId":"resp_0ce5441d29b9f05d016a5b420326f8819b96ddb51e01bc3c21","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b4205371c819bbc453b6504650873","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0INIwitQf0ER3dFdEvALVR4Ma_e4LrZKo76haFSCxClAo-XmnH2HnZQBxS9Z02GoPrXy2H5zhePG5l-UaEZaYhVSZLFle8anN7wI5Wl_-q89dyNqLnhW-yMqIQfJ7fCaJ53gAaCpkPXLMT0Mb0ILyBrUnho5rVddMlz3SzN1iZagb3d9cpaf-JNaTHmgicvvsW04zLL1qccMX6oyPfuwYG7kn6KuwnC_P5PcU67GoSJjB82Ah8Yh9KFqZ57MkRQ0pCEGHwnV_C3eOzccgjig3fN3fT7Yb8QyHzlh_4ouU8nAukiD1kVGYG783p1fGZy9zukV2DTwslzg1iy3DVbTvl9Cm5YOMJvuP08FHSCPKekyxO3vy4i5QsIsOZx11XN7x62-O63BMMN0mGwSgJdrvTFo17VGvo2vzCZ1SyadisbZ2tn4emDg300SEuN_U_5NkoZ-EM7bHiPgG64tcQilXuqBzRPJll2kybE6mpHsMDohz8ZUBoNn92xyCRGK7YMtjZhxtAq1n5nzBB3mLKhe5gxuB0yigihUmsvIaBp8vIId2ao1BHrbiJ-2jezDvS7AWvx0ySY4k0lYdw5787onm-YmsKpDbsZimd_-oNf-RrD_yb_KCbbiP5n4sWHIOHeRWwcUdtePf9UJEh70ciANk_J9Ag52_I3RPYsWdsZToBIiIKGnUexXNrHkV9pnGozqvEm5vHYfaHyL1PJLCdC7ezGT8NgAiTrOmkS_S7sWWzJMW0KUvMk1LPSebfA40uilSWDL_QBtfWgKdAtnGbC9qxLPPAwzSdY4tWyr7ZdJGp52nRflHSwexAu7WFu5q2jD8fhrZM2HVnvCw7LYtknmi7VLDbDzrIXv23zaKNu3wszZ_rJmMPrhBljx4FWC4M8j7Anhr994MrQ4pVV2RE9pl6hzG_Pcuv7jQTckZAoUr0aZfIS1tGp6AU355BJseKQ80r5DGqh3aNlZx7vpg6Ot2FrJtJgoKOuxbK4FAGGfEWoc5pKMByZmvdwDAAZCRIzhHvTHJ3Noc0auw6hDUG2TjbHDMCM1BydQYGtilWGnJwhSOADujeTZMMJ3LBpUELvLRMNUkBIPu5UgImDEW9fHlBgMxr5Mpt6qTd3L0oLkPgnZoyLoMhgiTjY9XPHjC23wNPOlhIqaHkdlxKA1G2de7aBtYxtjMbJyEW90j4ChoAIQkf4JL96eT5xZVFSJd3fI-oTnAUbtLgKrv2KRU0oj6ahcx6qQrRjOjhqR0KTFuovopBCTBhs0Cbn88NITkQCnuIMCWDsTuixFeEhP8lsRpoTlaQQ6mKHfnlrqDfrH6OP-hjA1WMCsQwTFNWhaLG2ExVf8hlImLqocbmL3my0abVaifiOkZA-BWqyZGAfzqlUPFXqtq5YPRju_XxhSt0Ir8YOmDmWE_V28dy9oQSf7aJaM5w44_xm0BOZFWqDx4TPprUJw9I-nlG2WHZEcwoqnM7QOfXu_7PqLVUFPuYIo1MNZD6gZS-zludQZk97ffScbqZS11vM9Uv8bsU3_HUBAKKWdPaHAQOTbY-5N6i5ax8NCSXRminy0IzxPGXScYAGNa2LyJGNT_-RhQXibIjaQSW--3xRmduiRYLppCEKNmDvskcxUFX_zVbzzWIC2FkenmDbsrk5ldFeMKoqP_TW1_M-yZ79pvzoKKzleFyibwV6ZaCLFAEWWkTbkJnaRxvPwyN8STlFfe7ev_fzGKni6nl61lEuqnt1vAdDzT9oWtZcOoyem3Va4-fA5yuodqkP5du5Of9ZJ3l9rLvP9rZ-mlxrXJJwnLmfX-GM-l5HZanakmoOfyaTE2lmDrrLh9u7GjnVFb_xqMiEzVZgAxVXgVxNgNf0KHiHxAB7_V7Yvl-GWigob3tI2R1TG4nMhGSd6ZaNGlYzxYCim1p2B-YkbE6kz5XK_eKlDnniT2jhDm_Vm_aDswR3wyocC3GuPH55YL0Am3Iq_hQY2Fa5vCal5aKd85jrQPoZccmZCb2QY3LxmyCDGCqnxbR1mWrTyM-WHCYH-NNCQH2-idUcxox0SuhdsVoDTYDZl4Q1H2WCtdPllWzZXSRHiDO2_DIol5-5cxSmEewDBZShtUsNbgZDmO4WbD78PDhhYp4x-yWLZMNhHs_NDWkxBBEsBH8OpZz3TW2G8TjidhMmceXkfIwxiHVBm675HCoaotVBC4ECLUmzHzxcL9I2WVxVS80_Kq35meuydpm46UOUBRe6nmeUSgpxT4-GuO1Pp9Wnpr2mlfpyP41c9mz2MLTeOxpnLSqzXioKcYSkO9kegzsoVginZBRj","summary":[{"type":"summary_text","text":"**Planning byte-level output reproduction**"},{"type":"summary_text","text":"**Designing exit coordination barrier**"}]},{"id":"fc_0ce5441d29b9f05d016a5b420d7a78819bbf1742b042bacc65","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating sink fanout helper\",\"pattern\":\"fn all_sinks|all_sinks\\\\(\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_lJDvQlY7floC6VtWSYstap7v","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b420d7a84819b954d6f92b486d977","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading sink helper\",\"path\":\"crates/spt-daemon/src/broker.rs\",\"selector\":\"1420-1485,1750-1795\"}","call_id":"call_pxpPuRZKFhIdlCApaQ9TyxLX","name":"read"},{"id":"fc_0ce5441d29b9f05d016a5b420d7a8c819bbb405e21001a8bad","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding terminal setup function\",\"pattern\":\"setup_terminal|restore_terminal|EnterAlternateScreen|LeaveAlternateScreen|terminal.clear\",\"path\":\"crates/spt/src/picker/mod.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_Fqxk5SS5RylmgTuOcuAKMjl2","name":"grep"},{"id":"fc_0ce5441d29b9f05d016a5b420d7a94819b88bdfe27ae27f759","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading picker terminal lifecycle\",\"path\":\"crates/spt/src/picker/mod.rs\",\"selector\":\"35-130\"}","call_id":"call_4SphK8TzuyIkhgGIIzjP3Wiu","name":"read"}]},"duration":13266.26479998231,"ttft":4909.924199998379,"contextSnapshot":{"promptTokens":208015,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_lJDvQlY7floC6VtWSYstap7v|fc_0ce5441d29b9f05d016a5b420d7a78819bbf1742b042bacc65","toolName":"grep","startedAt":"2026-07-18T09:06:21.569Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Locating sink fanout helper"},"id":"f8c3cfff","parentId":"66ffbe26","timestamp":"2026-07-18T09:06:21.569Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pxpPuRZKFhIdlCApaQ9TyxLX|fc_0ce5441d29b9f05d016a5b420d7a84819b954d6f92b486d977","toolName":"read","startedAt":"2026-07-18T09:06:21.569Z","args":{"path":"crates/spt-daemon/src/broker.rs"},"intent":"Reading sink helper"},"id":"a50913d8","parentId":"f8c3cfff","timestamp":"2026-07-18T09:06:21.569Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Fqxk5SS5RylmgTuOcuAKMjl2|fc_0ce5441d29b9f05d016a5b420d7a8c819bbb405e21001a8bad","toolName":"grep","startedAt":"2026-07-18T09:06:21.570Z","args":{"path":"crates/spt/src/picker/mod.rs"},"intent":"Finding terminal setup function"},"id":"0a26aeab","parentId":"a50913d8","timestamp":"2026-07-18T09:06:21.570Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_4SphK8TzuyIkhgGIIzjP3Wiu|fc_0ce5441d29b9f05d016a5b420d7a94819b88bdfe27ae27f759","toolName":"read","startedAt":"2026-07-18T09:06:21.570Z","args":{"path":"crates/spt/src/picker/mod.rs"},"intent":"Reading picker terminal lifecycle"},"id":"cecb1a26","parentId":"0a26aeab","timestamp":"2026-07-18T09:06:21.570Z"}
{"type":"message","id":"b6062c83","parentId":"cecb1a26","timestamp":"2026-07-18T09:06:21.571Z","message":{"role":"toolResult","toolCallId":"call_lJDvQlY7floC6VtWSYstap7v|fc_0ce5441d29b9f05d016a5b420d7a78819bbf1742b042bacc65","toolName":"grep","content":[{"type":"text","text":" 1439|    /// an `exit` frame.\n*1440|    fn all_sinks(&self) -> Vec<SharedSend> {\n 1441|        let mut v: Vec<SharedSend> = Vec::new();\n 1442|        if let Some(c) = &self.controller {\n 1443|            v.push(Arc::clone(&c.send));\n...\n 3768|                // the log lock).\n*3769|                let sinks = recover_log(&log_exit).all_sinks();\n 3770|                for sink in sinks {\n 3771|                    // Bounded conn write (round-4): a wedged sink can no longer\n 3772|                    // park the exit-waiter thread indefinitely."}],"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":2,"fileCount":1,"files":["crates/spt-daemon/src/broker.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/broker.rs","count":2}],"truncated":false,"displayContent":" 1439│    /// an `exit` frame.\n*1440│    fn all_sinks(&self) -> Vec<SharedSend> {\n 1441│        let mut v: Vec<SharedSend> = Vec::new();\n 1442│        if let Some(c) = &self.controller {\n 1443│            v.push(Arc::clone(&c.send));\n     │...\n 3768│                // the log lock).\n*3769│                let sinks = recover_log(&log_exit).all_sinks();\n 3770│                for sink in sinks {\n 3771│                    // Bounded conn write (round-4): a wedged sink can no longer\n 3772│                    // park the exit-waiter thread indefinitely."},"isError":false,"timestamp":1784365581571}}
{"type":"message","id":"782cf5dc","parentId":"b6062c83","timestamp":"2026-07-18T09:06:21.572Z","message":{"role":"toolResult","toolCallId":"call_Fqxk5SS5RylmgTuOcuAKMjl2|fc_0ce5441d29b9f05d016a5b420d7a8c819bbb405e21001a8bad","toolName":"grep","content":[{"type":"text","text":" 32|use crossterm::terminal::{\n*33|    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle,\n 34|};\n 35|use ratatui::backend::CrosstermBackend;\n 36|use ratatui::Terminal;\n...\n 71|\n*72|    let mut terminal = match setup_terminal() {\n 73|        Ok(t) => t,\n 74|        Err(e) => {\n 75|            eprintln!(\"PICKER_TTY_FAIL: {e}\");\n...\n 81|    // own raw mode themselves for the attach.\n*82|    restore_terminal(&mut terminal);\n 83|\n 84|    match outcome {\n 85|        Ok(outcome) => dispatch(outcome),\n...\n 94|\n*95|fn setup_terminal() -> io::Result<Term> {\n 96|    enable_raw_mode()?;\n 97|    let mut stdout = io::stdout();\n 98|    // B-5 (F029): title the picker window/tab so `spt endpoint run` is identifiable\n...\n 100|    // to restore it on exit (accepted). This runs ONLY on the interactive picker\n*101|    // path (run() → setup_terminal); the non-interactive REQ-HOST-RUN-1 flow never\n 102|    // reaches here, so a headless invocation never retitles the operator's terminal.\n 103|    // [impl->REQ-PICKER-WINDOW-TITLE]\n*104|    crossterm::execute!(stdout, SetTitle(\"SPT Endpoint Picker\"), EnterAlternateScreen)?;\n 105|    Terminal::new(CrosstermBackend::new(stdout))\n 106|}\n 107|\n*108|fn restore_terminal(terminal: &mut Term) {\n 109|    let _ = disable_raw_mode();\n*110|    let _ = crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen);\n 111|    let _ = terminal.show_cursor();\n 112|}\n 113|"}],"details":{"scopePath":"crates/spt/src/picker/mod.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\picker\\mod.rs","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":8,"fileCount":1,"files":["crates/spt/src/picker/mod.rs"],"fileMatches":[{"path":"crates/spt/src/picker/mod.rs","count":8}],"truncated":false,"displayContent":"  32│use crossterm::terminal::{\n *33│    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle,\n  34│};\n  35│use ratatui::backend::CrosstermBackend;\n  36│use ratatui::Terminal;\n    │...\n  71│\n *72│    let mut terminal = match setup_terminal() {\n  73│        Ok(t) => t,\n  74│        Err(e) => {\n  75│            eprintln!(\"PICKER_TTY_FAIL: {e}\");\n    │...\n  81│    // own raw mode themselves for the attach.\n *82│    restore_terminal(&mut terminal);\n  83│\n  84│    match outcome {\n  85│        Ok(outcome) => dispatch(outcome),\n    │...\n  94│\n *95│fn setup_terminal() -> io::Result<Term> {\n  96│    enable_raw_mode()?;\n  97│    let mut stdout = io::stdout();\n  98│    // B-5 (F029): title the picker window/tab so `spt endpoint run` is identifiable\n    │...\n 100│    // to restore it on exit (accepted). This runs ONLY on the interactive picker\n*101│    // path (run() → setup_terminal); the non-interactive REQ-HOST-RUN-1 flow never\n 102│    // reaches here, so a headless invocation never retitles the operator's terminal.\n 103│    // [impl->REQ-PICKER-WINDOW-TITLE]\n*104│    crossterm::execute!(stdout, SetTitle(\"SPT Endpoint Picker\"), EnterAlternateScreen)?;\n 105│    Terminal::new(CrosstermBackend::new(stdout))\n 106│}\n 107│\n*108│fn restore_terminal(terminal: &mut Term) {\n 109│    let _ = disable_raw_mode();\n*110│    let _ = crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen);\n 111│    let _ = terminal.show_cursor();\n 112│}\n 113│"},"isError":false,"timestamp":1784365581572}}
{"type":"message","id":"a764017a","parentId":"782cf5dc","timestamp":"2026-07-18T09:06:21.615Z","message":{"role":"toolResult","toolCallId":"call_pxpPuRZKFhIdlCApaQ9TyxLX|fc_0ce5441d29b9f05d016a5b420d7a84819b954d6f92b486d977","toolName":"read","content":[{"type":"text","text":"    fn detach_if(&mut self, sub: &SharedSend) {\n…\n        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n        // Bounded detach record (F-039 leg d) — only when a role actually changed.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if is_ctrl || self.viewers.len() != before {\n            sub.lifecycle_event(\n                \"session-detach\",\n                &format!(\n                    \"session={} was_controller={is_ctrl} viewers_dropped={}\",\n                    self.session_id,\n                    before - self.viewers.len()\n                ),\n            );\n        }\n        if self.viewers.len() != before {\n            self.stamp_viewer_count();\n        }\n    }\n\n    /// Every sink currently attached (controller + viewers) — the recipients of\n    /// an `exit` frame.\n    fn all_sinks(&self) -> Vec<SharedSend> {\n        let mut v: Vec<SharedSend> = Vec::new();\n        if let Some(c) = &self.controller {\n            v.push(Arc::clone(&c.send));\n        }\n        for sink in self.viewers.values() {\n            v.push(Arc::clone(&sink.send));\n        }\n        v\n    }\n\n    /// Stamp the perch's `driven_by` to the current controller's identity (the\n    /// broker is the single writer — resolves the clear-race). The remote-drive\n    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced\n    /// operator's teardown can never wipe the new controller's marker.\n    /// Best-effort: a session served without a perch (raw broker tests) has\n    /// nowhere to record.\n    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]\n    fn stamp_driven_by(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        // `driven_by` is REMOTE-only: `controller_by()` is `None` for a local\n        // controller (its `by` is `None`), and it MUST stay that way — a local\n        // controller latching driven_by trips REQ-HAZARD-DRIVEN-BY-SELFHEAL (KH 7.15).\n        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());\n        // #3 (REQ-GOSSIP-CONTROLLED-ANY): stamp a SEPARATE any-controller datum so a\n        // LOCALLY-driven endpoint (driven_by = None) still gossips as controlled to\n        // remote viewers. `controller_by` answers \"is a remote node driving me\";\n        // `controller.is_some()` answers \"is ANYONE driving me\".\n        // [impl->REQ-GOSSIP-CONTROLLED-ANY]\n        let _ = spt_store::info::set_controlled(&perch, self.controller.is_some());\n    }\n\n    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the\n    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].\n    // [impl->REQ-RCVIEW-1]\n    fn stamp_viewer_count(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);\n    }\n\n…\nfn controller_writer(\n…\n) {\n…\n    while let Ok(msg) = rx.recv() {\n…\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…\n}"}],"details":{"displayContent":{"text":"    fn detach_if(&mut self, sub: &SharedSend) {\n…\n        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));\n        // Bounded detach record (F-039 leg d) — only when a role actually changed.\n        // [impl->REQ-CONN-POISON-ATTRIBUTION]\n        if is_ctrl || self.viewers.len() != before {\n            sub.lifecycle_event(\n                \"session-detach\",\n                &format!(\n                    \"session={} was_controller={is_ctrl} viewers_dropped={}\",\n                    self.session_id,\n                    before - self.viewers.len()\n                ),\n            );\n        }\n        if self.viewers.len() != before {\n            self.stamp_viewer_count();\n        }\n    }\n\n    /// Every sink currently attached (controller + viewers) — the recipients of\n    /// an `exit` frame.\n    fn all_sinks(&self) -> Vec<SharedSend> {\n        let mut v: Vec<SharedSend> = Vec::new();\n        if let Some(c) = &self.controller {\n            v.push(Arc::clone(&c.send));\n        }\n        for sink in self.viewers.values() {\n            v.push(Arc::clone(&sink.send));\n        }\n        v\n    }\n\n    /// Stamp the perch's `driven_by` to the current controller's identity (the\n    /// broker is the single writer — resolves the clear-race). The remote-drive\n    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced\n    /// operator's teardown can never wipe the new controller's marker.\n    /// Best-effort: a session served without a perch (raw broker tests) has\n    /// nowhere to record.\n    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]\n    fn stamp_driven_by(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        // `driven_by` is REMOTE-only: `controller_by()` is `None` for a local\n        // controller (its `by` is `None`), and it MUST stay that way — a local\n        // controller latching driven_by trips REQ-HAZARD-DRIVEN-BY-SELFHEAL (KH 7.15).\n        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());\n        // #3 (REQ-GOSSIP-CONTROLLED-ANY): stamp a SEPARATE any-controller datum so a\n        // LOCALLY-driven endpoint (driven_by = None) still gossips as controlled to\n        // remote viewers. `controller_by` answers \"is a remote node driving me\";\n        // `controller.is_some()` answers \"is ANYONE driving me\".\n        // [impl->REQ-GOSSIP-CONTROLLED-ANY]\n        let _ = spt_store::info::set_controlled(&perch, self.controller.is_some());\n    }\n\n    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the\n    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].\n    // [impl->REQ-RCVIEW-1]\n    fn stamp_viewer_count(&self) {\n        if self.endpoint.is_empty() {\n            return;\n        }\n        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);\n        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);\n    }\n\n…\nfn controller_writer(\n…\n) {\n…\n    while let Ok(msg) = rx.recv() {\n…\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…\n}","startLine":1411,"lineNumbers":[1411,null,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,null,1671,null,1680,null,1735,null,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,null,1806]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\broker.rs"}}},"isError":false,"timestamp":1784365581615}}
{"type":"message","id":"1769c8b2","parentId":"a764017a","timestamp":"2026-07-18T09:06:21.623Z","message":{"role":"toolResult","toolCallId":"call_4SphK8TzuyIkhgGIIzjP3Wiu|fc_0ce5441d29b9f05d016a5b420d7a94819b88bdfe27ae27f759","toolName":"read","content":[{"type":"text","text":"use crossterm::terminal::{\n…\n};\nuse ratatui::backend::CrosstermBackend;\nuse ratatui::Terminal;\n\nuse model::{ConfirmOption, Outcome, PickerModel, RunMode, Screen};\nuse shortcut::WriteOutcome;\n\n/// Entry point for `spt endpoint run` when it is NOT fully flagged. Gathers the\n/// data, drives the TUI, and dispatches the chosen outcome through the existing\n/// bringup fns. Returns the process exit code.\npub fn run(prefill_adapter: Option<String>, prefill_id: Option<String>) -> i32 {\n    // Both halves in one derivation pass: the slug KEYS the history filter,\n    // the display RENDERS on the tab (REQ-PICKER-PROJECT-DISPLAY-NAME).\n    let (project_name, project_display) = data::project_name_and_display();\n    let adapter_rows = data::adapter_options();\n    let endpoints = data::gather_endpoints();\n    // The MRU-ordered member subnets for the create-new home layer (< 2 ⇒ the\n    // model skips the layer; single-subnet/unpaired auto-homes). REQ-RUN-PICKER-HOME.\n    let home_subnets = data::home_subnet_options();\n    let mut model = PickerModel::new(\n        project_name,\n        adapter_rows,\n        endpoints,\n        prefill_adapter,\n        prefill_id,\n        home_subnets,\n    );\n    // The launch dir — the \"Here: <dir>\" Choose-project row + the mismatch test\n    // (REQ-PICKER-START-PROJECT-CHOICE).\n    model.run_cwd = std::env::current_dir()\n        .map(|p| p.to_string_lossy().into_owned())\n        .unwrap_or_default();\n    // The tab renders (REMOTE-TRUTH rider): the project DISPLAY name + this\n    // node's label — stamped like run_cwd; the match key stays the slug.\n    // [impl->REQ-PICKER-PROJECT-DISPLAY-NAME]\n    model.project_display = project_display;\n    model.local_tab_label = data::own_node_tab_label();\n\n    let mut terminal = match setup_terminal() {\n        Ok(t) => t,\n        Err(e) => {\n            eprintln!(\"PICKER_TTY_FAIL: {e}\");\n            return 1;\n        }\n    };\n    let outcome = event_loop(&mut terminal, &mut model);\n    // Restore the terminal BEFORE dispatching — cmd_endpoint_run / the rc pump\n    // own raw mode themselves for the attach.\n    restore_terminal(&mut terminal);\n\n    match outcome {\n        Ok(outcome) => dispatch(outcome),\n        Err(e) => {\n            eprintln!(\"PICKER_FAIL: {e}\");\n            1\n        }\n    }\n}\n\ntype Term = Terminal<CrosstermBackend<Stdout>>;\n\nfn setup_terminal() -> io::Result<Term> {\n    enable_raw_mode()?;\n    let mut stdout = io::stdout();\n    // B-5 (F029): title the picker window/tab so `spt endpoint run` is identifiable\n    // among many terminals. Set-only — crossterm can't cheaply read the prior title\n    // to restore it on exit (accepted). This runs ONLY on the interactive picker\n    // path (run() → setup_terminal); the non-interactive REQ-HOST-RUN-1 flow never\n    // reaches here, so a headless invocation never retitles the operator's terminal.\n    // [impl->REQ-PICKER-WINDOW-TITLE]\n    crossterm::execute!(stdout, SetTitle(\"SPT Endpoint Picker\"), EnterAlternateScreen)?;\n    Terminal::new(CrosstermBackend::new(stdout))\n}\n\nfn restore_terminal(terminal: &mut Term) {\n    let _ = disable_raw_mode();\n    let _ = crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen);\n    let _ = terminal.show_cursor();\n}\n\n/// The live key loop. Returns the chosen [`Outcome`]; `Outcome::Shortcut` is\n/// handled inline (write the file, flash, stay) so it never escapes the loop.\nfn event_loop(terminal: &mut Term, model: &mut PickerModel) -> io::Result<Outcome> {\n    loop {\n        terminal.draw(|f| view::render(model, f))?;\n        let Event::Key(key) = event::read()? else {\n            continue;\n        };\n        // Windows fires Press AND Release; only act on Press.\n        if key.kind != KeyEventKind::Press {\n            continue;\n        }\n        // Ctrl-C always cancels.\n        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {\n            return Ok(Outcome::Cancelled);\n        }\n        // Any key clears a stale flash.\n        model.flash = None;\n\n        if let Some(outcome) = handle_key(model, key.code) {\n…\n        }\n    }\n}\n\n[Showing lines 34-133 of 606. Use :134 to continue]"}],"details":{"truncation":{"content":"};\r\nuse ratatui::backend::CrosstermBackend;\r\nuse ratatui::Terminal;\r\n\r\nuse model::{ConfirmOption, Outcome, PickerModel, RunMode, Screen};\r\nuse shortcut::WriteOutcome;\r\n\r\n/// Entry point for `spt endpoint run` when it is NOT fully flagged. Gathers the\r\n/// data, drives the TUI, and dispatches the chosen outcome through the existing\r\n/// bringup fns. Returns the process exit code.\r\npub fn run(prefill_adapter: Option<String>, prefill_id: Option<String>) -> i32 {\r\n    // Both halves in one derivation pass: the slug KEYS the history filter,\r\n    // the display RENDERS on the tab (REQ-PICKER-PROJECT-DISPLAY-NAME).\r\n    let (project_name, project_display) = data::project_name_and_display();\r\n    let adapter_rows = data::adapter_options();\r\n    let endpoints = data::gather_endpoints();\r\n    // The MRU-ordered member subnets for the create-new home layer (< 2 ⇒ the\r\n    // model skips the layer; single-subnet/unpaired auto-homes). REQ-RUN-PICKER-HOME.\r\n    let home_subnets = data::home_subnet_options();\r\n    let mut model = PickerModel::new(\r\n        project_name,\r\n        adapter_rows,\r\n        endpoints,\r\n        prefill_adapter,\r\n        prefill_id,\r\n        home_subnets,\r\n    );\r\n    // The launch dir — the \"Here: <dir>\" Choose-project row + the mismatch test\r\n    // (REQ-PICKER-START-PROJECT-CHOICE).\r\n    model.run_cwd = std::env::current_dir()\r\n        .map(|p| p.to_string_lossy().into_owned())\r\n        .unwrap_or_default();\r\n    // The tab renders (REMOTE-TRUTH rider): the project DISPLAY name + this\r\n    // node's label — stamped like run_cwd; the match key stays the slug.\r\n    // [impl->REQ-PICKER-PROJECT-DISPLAY-NAME]\r\n    model.project_display = project_display;\r\n    model.local_tab_label = data::own_node_tab_label();\r\n\r\n    let mut terminal = match setup_terminal() {\r\n        Ok(t) => t,\r\n        Err(e) => {\r\n            eprintln!(\"PICKER_TTY_FAIL: {e}\");\r\n            return 1;\r\n        }\r\n    };\r\n    let outcome = event_loop(&mut terminal, &mut model);\r\n    // Restore the terminal BEFORE dispatching — cmd_endpoint_run / the rc pump\r\n    // own raw mode themselves for the attach.\r\n    restore_terminal(&mut terminal);\r\n\r\n    match outcome {\r\n        Ok(outcome) => dispatch(outcome),\r\n        Err(e) => {\r\n            eprintln!(\"PICKER_FAIL: {e}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\ntype Term = Terminal<CrosstermBackend<Stdout>>;\r\n\r\nfn setup_terminal() -> io::Result<Term> {\r\n    enable_raw_mode()?;\r\n    let mut stdout = io::stdout();\r\n    // B-5 (F029): title the picker window/tab so `spt endpoint run` is identifiable\r\n    // among many terminals. Set-only — crossterm can't cheaply read the prior title\r\n    // to restore it on exit (accepted). This runs ONLY on the interactive picker\r\n    // path (run() → setup_terminal); the non-interactive REQ-HOST-RUN-1 flow never\r\n    // reaches here, so a headless invocation never retitles the operator's terminal.\r\n    // [impl->REQ-PICKER-WINDOW-TITLE]\r\n    crossterm::execute!(stdout, SetTitle(\"SPT Endpoint Picker\"), EnterAlternateScreen)?;\r\n    Terminal::new(CrosstermBackend::new(stdout))\r\n}\r\n\r\nfn restore_terminal(terminal: &mut Term) {\r\n    let _ = disable_raw_mode();\r\n    let _ = crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen);\r\n    let _ = terminal.show_cursor();\r\n}\r\n\r\n/// The live key loop. Returns the chosen [`Outcome`]; `Outcome::Shortcut` is\r\n/// handled inline (write the file, flash, stay) so it never escapes the loop.\r\nfn event_loop(terminal: &mut Term, model: &mut PickerModel) -> io::Result<Outcome> {\r\n    loop {\r\n        terminal.draw(|f| view::render(model, f))?;\r\n        let Event::Key(key) = event::read()? else {\r\n            continue;\r\n        };\r\n        // Windows fires Press AND Release; only act on Press.\r\n        if key.kind != KeyEventKind::Press {\r\n            continue;\r\n        }\r\n        // Ctrl-C always cancels.\r\n        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {\r\n            return Ok(Outcome::Cancelled);\r\n        }\r\n        // Any key clears a stale flash.\r\n        model.flash = None;\r\n\r\n        if let Some(outcome) = handle_key(model, key.code) {\r","truncated":true,"truncatedBy":"lines","totalLines":573,"totalBytes":4166,"outputLines":100,"outputBytes":4166,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"use crossterm::terminal::{\n…\n};\r\nuse ratatui::backend::CrosstermBackend;\r\nuse ratatui::Terminal;\r\n\r\nuse model::{ConfirmOption, Outcome, PickerModel, RunMode, Screen};\r\nuse shortcut::WriteOutcome;\r\n\r\n/// Entry point for `spt endpoint run` when it is NOT fully flagged. Gathers the\r\n/// data, drives the TUI, and dispatches the chosen outcome through the existing\r\n/// bringup fns. Returns the process exit code.\r\npub fn run(prefill_adapter: Option<String>, prefill_id: Option<String>) -> i32 {\r\n    // Both halves in one derivation pass: the slug KEYS the history filter,\r\n    // the display RENDERS on the tab (REQ-PICKER-PROJECT-DISPLAY-NAME).\r\n    let (project_name, project_display) = data::project_name_and_display();\r\n    let adapter_rows = data::adapter_options();\r\n    let endpoints = data::gather_endpoints();\r\n    // The MRU-ordered member subnets for the create-new home layer (< 2 ⇒ the\r\n    // model skips the layer; single-subnet/unpaired auto-homes). REQ-RUN-PICKER-HOME.\r\n    let home_subnets = data::home_subnet_options();\r\n    let mut model = PickerModel::new(\r\n        project_name,\r\n        adapter_rows,\r\n        endpoints,\r\n        prefill_adapter,\r\n        prefill_id,\r\n        home_subnets,\r\n    );\r\n    // The launch dir — the \"Here: <dir>\" Choose-project row + the mismatch test\r\n    // (REQ-PICKER-START-PROJECT-CHOICE).\r\n    model.run_cwd = std::env::current_dir()\r\n        .map(|p| p.to_string_lossy().into_owned())\r\n        .unwrap_or_default();\r\n    // The tab renders (REMOTE-TRUTH rider): the project DISPLAY name + this\r\n    // node's label — stamped like run_cwd; the match key stays the slug.\r\n    // [impl->REQ-PICKER-PROJECT-DISPLAY-NAME]\r\n    model.project_display = project_display;\r\n    model.local_tab_label = data::own_node_tab_label();\r\n\r\n    let mut terminal = match setup_terminal() {\r\n        Ok(t) => t,\r\n        Err(e) => {\r\n            eprintln!(\"PICKER_TTY_FAIL: {e}\");\r\n            return 1;\r\n        }\r\n    };\r\n    let outcome = event_loop(&mut terminal, &mut model);\r\n    // Restore the terminal BEFORE dispatching — cmd_endpoint_run / the rc pump\r\n    // own raw mode themselves for the attach.\r\n    restore_terminal(&mut terminal);\r\n\r\n    match outcome {\r\n        Ok(outcome) => dispatch(outcome),\r\n        Err(e) => {\r\n            eprintln!(\"PICKER_FAIL: {e}\");\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\ntype Term = Terminal<CrosstermBackend<Stdout>>;\r\n\r\nfn setup_terminal() -> io::Result<Term> {\r\n    enable_raw_mode()?;\r\n    let mut stdout = io::stdout();\r\n    // B-5 (F029): title the picker window/tab so `spt endpoint run` is identifiable\r\n    // among many terminals. Set-only — crossterm can't cheaply read the prior title\r\n    // to restore it on exit (accepted). This runs ONLY on the interactive picker\r\n    // path (run() → setup_terminal); the non-interactive REQ-HOST-RUN-1 flow never\r\n    // reaches here, so a headless invocation never retitles the operator's terminal.\r\n    // [impl->REQ-PICKER-WINDOW-TITLE]\r\n    crossterm::execute!(stdout, SetTitle(\"SPT Endpoint Picker\"), EnterAlternateScreen)?;\r\n    Terminal::new(CrosstermBackend::new(stdout))\r\n}\r\n\r\nfn restore_terminal(terminal: &mut Term) {\r\n    let _ = disable_raw_mode();\r\n    let _ = crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen);\r\n    let _ = terminal.show_cursor();\r\n}\r\n\r\n/// The live key loop. Returns the chosen [`Outcome`]; `Outcome::Shortcut` is\r\n/// handled inline (write the file, flash, stay) so it never escapes the loop.\r\nfn event_loop(terminal: &mut Term, model: &mut PickerModel) -> io::Result<Outcome> {\r\n    loop {\r\n        terminal.draw(|f| view::render(model, f))?;\r\n        let Event::Key(key) = event::read()? else {\r\n            continue;\r\n        };\r\n        // Windows fires Press AND Release; only act on Press.\r\n        if key.kind != KeyEventKind::Press {\r\n            continue;\r\n        }\r\n        // Ctrl-C always cancels.\r\n        if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {\r\n            return Ok(Outcome::Cancelled);\r\n        }\r\n        // Any key clears a stale flash.\r\n        model.flash = None;\r\n\r\n        if let Some(outcome) = handle_key(model, key.code) {\r\n…\n        }\n    }\n}","startLine":32,"lineNumbers":[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,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,null,177,178,179]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\picker\\mod.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":606,"totalBytes":4166,"outputLines":100,"outputBytes":4166,"shownRange":{"start":34,"end":133},"nextOffset":134}}},"isError":false,"timestamp":1784365581623}}
{"type":"message","id":"e38284bf","parentId":"1769c8b2","timestamp":"2026-07-18T09:08:42.513Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Analyzing connection timeout handling**\n\n**Identifying scroll region restoration gaps**\n\n**Fixing scroll region reset on repaint**"},{"type":"thinking","thinking":"**Fixing cursor handling with DECSTBM**\n\n**Proposing deterministic repaint and teardown tests**\n\n**Identifying broker exit race causing missing whitespace writes**"},{"type":"thinking","thinking":"**Planning orderly exit write enqueue**\n\n**Designing coordinated drain completion signaling**\n\n**Clarifying report architecture and schema**"},{"type":"thinking","thinking":"**Mapping function call sequence**\n\n**Verifying terminal output byte sequences**"},{"type":"thinking","thinking":"**Analyzing terminal escape sequence handling**\n\n**Clarifying reconnect behavior and error handling**"},{"type":"toolCall","id":"call_YDHytc3RzR8YEiK4HQkCVLud|fc_0ce5441d29b9f05d016a5b424d1c50819bb4166adf88f693d3","name":"yield","arguments":{"type":"result","result":{"data":{"summary":"Root cause is twofold, with one additional deterministic state-desynchronization seam. (1) The broker/rc transport does not filter spaces: live PTY chunks are forwarded byte-for-byte. Cold attach intentionally omits trailing default spaces and wholly blank rows in `ScreenGrid::render_repaint`, but only after emitting `CSI 2 J`, so that omission is semantically correct on a VT-capable terminal. (2) Final PTY output is not ordered before `Exit`: the drain queues output through the controller/viewer writer, while the independent exit waiter writes `KIND_EXIT` directly to the same physical connection. `Exit` can overtake queued final output, and both `serve_attach` and the rc pump terminate immediately on it. Thus a final `EL`, spaces, `?25h`, `?1049l`, or other teardown sequence can be stranded. The repository already acknowledges this race in `crates/spt-daemon/tests/broker.rs:376-403`. (3) rc has no unconditional terminal-mode teardown. With the status row disabled, every final path—detach, child exit, displacement, 30-second reconnect give-up, first-event stall, and fatal error—drops raw/input modes and prints prose, but emits no SGR reset, cursor-show, scroll-region reset, or alternate-screen leave. A child that exits or disconnects without completing its own teardown therefore leaves the operator terminal dirty. Separately, a cold repaint tracks but does not replay DECSTBM scroll margins, so subsequent live raw output can be interpreted against a different scroll region than the server grid. Picker rendering does not share the PTY/grid path: it uses ratatui’s local previous-buffer diff. Its stale-glyph cause is distinct—`cmd_endpoint_purge` writes to stderr while ratatui still owns the alternate screen, invalidating ratatui’s previous-buffer model; the next diff legitimately omits unchanged blank cells. The only commonality is mutation of the same physical terminal outside the renderer’s state model.","files":[{"path":"crates/spt-term/src/reader.rs","description":"`Drain::spawn` at lines 118-161 reads up to 8192 bytes, scans DSR without removing it, calls `sink(chunk)`, and counts exactly `n`; there is no whitespace/ANSI filtering. This is the PTY-byte origin of the broker path."},{"path":"crates/spt-daemon/src/broker.rs","description":"`OutputLog::append` at lines 903-968 clones the exact chunk into the ring and `output_envelope`, fans it to controller/viewers, then advances `ScreenGrid`. `repaint_initial` at 821-825 substitutes one synthesized repaint only for cold `from_seq == 0`. `become_controller` at 1016-1064 creates the initial batch/writer. `controller_writer` at 1675-1778 serializes queued output. The exit waiter at 3757-3792 independently calls `all_sinks()` and directly `sink.write(&KIND_EXIT)` before removing the session; that direct write is not FIFO-ordered behind the controller/viewer output queues and does not wait for drain EOF. `BrokerConn` cleanup in `handle_conn` at 3465-3500 detaches session subscriptions and FINs/terminal-retires ConnectionBound rc streams."},{"path":"crates/spt-daemon/tests/broker.rs","description":"Lines 376-403 explicitly document and work around the production ordering fact: `EXIT notification ... can arrive BEFORE the OUTPUT frame`; the test continues draining after Exit. The rc production pump does not. This is direct repository evidence for the final-tail loss race and a natural regression-test location."},{"path":"crates/spt-term/src/screen.rs","description":"`Cell::is_default_blank` at 141-146 marks only default-pen spaces trimmable. `erase_display`/`erase_line`/`erase_chars` at 347-416 retain erased cells in the model. `render_repaint` at 577-623 emits matching main/alt selection, optional title, `CSI 0 m`, `CSI 2 J`, home, then uses `rposition` and `continue` at 597-599 to omit trailing default blanks and blank rows. Because ED2 precedes it, those omissions are correct for cold repaint; non-default-background blank cells are emitted. However `scroll_top`/`scroll_bottom` are tracked and DECSTBM is parsed at 790-802, but `render_repaint` never emits `CSI <top>;<bottom> r`, leaving client and grid scroll behavior divergent after attach."},{"path":"crates/spt-daemon/src/msg.rs","description":"`OutputEvent` at 548-558 stores arbitrary PTY bytes as base64. `encode_bytes`, `decode_bytes`, and `output_envelope` at 1164-1184 are byte-preserving; no trim, UTF-8 conversion, or whitespace classification occurs."},{"path":"crates/spt-daemon/src/brain.rs","description":"`Brain::read_event_until` at 686-755 decodes `OutputEvent` bytes unchanged and applies only sequence dedup/gap rules. `attach`/`attach_as` at 958-1008 arm the cold-repaint pseudo-sequence baseline. No character-level filter exists here."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` forwards `BrokerEvent::Output` unchanged via base64 at 600-609, but on broker `Exit` at 610-617 immediately sends `AttachRecord::Exit` with FIN and returns. It cannot recover a final output frame that arrives after broker Exit. Net-stream EOF cleanup at 568-593 explicitly releases the generation-owned broker role."},{"path":"crates/spt-net/src/net/attach.rs","description":"`AttachRecord::Output`/`Exit` at lines 103-120 are independent ordered NDJSON records over the stream; output bytes remain base64. The protocol has no `exit_after_seq`/drain watermark that would let the operator defer Exit until the output cursor reaches the final sequence."},{"path":"crates/spt/src/rc.rs","description":"`pump` at 1837-2020 decodes each Output and calls `stdout.write_all(&bytes)` verbatim at 1972-1993, but returns immediately on `AttachRecord::Exit` at 1995. `run_attach_inner` cleanup at 1254-1268 restores only the optional status row (currently disabled) and drops `RawGuard`; `RawGuard::drop` at 841-856 restores crossterm input mode/mouse/Windows console mode, not child-controlled VT state. Final branches then print prose. `reconnect_banner_bytes` at 1719-1744 does ED2+home, while the reconnect window is 30 seconds at 1659-1663; successful reconnect gets a fresh repaint, but give-up has no terminal restoration. `FIRST_EVENT_GRACE` at 1792-1805 is only a never-received-any-event backstop, not an idle/disconnect timer after the viewport becomes live."},{"path":"crates/spt-daemon/src/conn.rs","description":"`BrokerConn::write_gated` at 493-610 applies the broker’s absolute 15-second write bound, poisons the entire physical framed connection on timeout/partial-write risk, aborts/severs it, and causes `handle_conn` EOF cleanup. This correctly avoids reusing a potentially partial length-prefixed frame, but it cannot restore local terminal VT modes; that responsibility remains in rc."},{"path":"crates/spt/src/picker/mod.rs","description":"`setup_terminal`/`restore_terminal` at 95-112 use `ratatui::Terminal<CrosstermBackend<Stdout>>` directly and are unrelated to `spt-term::ScreenGrid`. `event_loop` at 165-178 invokes purge inline while ratatui still owns raw/alternate-screen state, then continues drawing with ratatui’s stale previous Buffer."},{"path":"crates/spt/src/cli.rs","description":"`cmd_endpoint_purge` at 12707-12826 emits multiple `eprintln!` paths, including `PURGED:{id}` at 12825. When called inline by picker, this out-of-band write mutates the same alternate-screen cursor/display without updating ratatui’s diff baseline, producing stale characters where the next diff skips unchanged spaces."},{"path":"docs/adr/0031-server-side-screen-grid-render-repaint.md","description":"Governing contract: the grid is the OS-neutral render-replay layer; cold attach gets one synthesized current-screen repaint while live frames remain raw. It confirms picker/PTY content semantics should not be conflated."},{"path":"docs/KNOWN-HAZARDS.md","description":"Hazards 7.27 and the bounded-write sections describe broker exit/control cleanup and 15-second poisoned-connection retirement. They cover control stamps and transport wedges, but not ordering final output before Exit or restoring the operator’s VT state."}],"architecture":"## End-to-end byte path\n\n```text\nPTY child\n  -> portable_pty master\n  -> spt_term::Drain::spawn(reader.read)\n  -> broker OutputLog::append\n       -> raw ring VecDeque<(seq, Vec<u8>)>\n       -> controller/viewer writer queue\n       -> ScreenGrid::advance (state projection, only used for cold attach)\n  -> OutputEvent { seq, data_b64 }\n  -> Brain::read_event_until\n  -> serve_attach: AttachRecord::Output { seq, data_b64 } NDJSON\n  -> broker-owned loopback/QUIC net stream\n  -> rc AttachDecoder\n  -> decode_bytes\n  -> stdout.write_all(raw bytes)\n```\n\nThere is no character diff or whitespace filter on the live path. The only content transformation is the cold-attach branch:\n\n```text\nfrom_seq == 0 && next_seq > 0\n  -> ScreenGrid::render_repaint()\n  -> one Output frame at pseudo-seq next_seq - 1\n```\n\n## Byte-level reproductions\n\n### 1. Spaces and erase sequences survive the live path\n\nHave the PTY emit:\n\n```text\nhex: 41 20 20 1b 5b 32 4b 0d\ntext: A␠␠ ESC [ 2 K CR\n```\n\n`Drain::spawn` passes the exact 8-byte slice; `OutputLog::append` stores `bytes.to_vec()`; each envelope base64-encodes and decodes the same bytes; rc writes exactly those 8 bytes. A capture at rc stdout must equal the input. Any mismatch here falsifies the current code reading.\n\nOn a cold attach, the original `CSI 2 K` and the two trailing spaces are consumed into grid state rather than replayed literally. The synthesized frame begins with one of `ESC[?1049h` / `ESC[?1049l`, then `ESC[0m ESC[2J ESC[H`. Default blank rows and trailing default spaces are omitted after that full clear. Therefore a terminal initialized with `XXXXXXXXXXXXXXXX` must become blank except for modeled nonblank/non-default-background cells. If it does not, the failure is VT-output interpretation, not missing whitespace in the renderer.\n\n### 2. Final output can be overtaken by Exit\n\nUse a short-lived PTY child whose final single write is:\n\n```text\nhex: 58 58 58 58 1b 5b 32 4b 1b 5b 3f 32 35 68 1b 5b 3f 31 30 34 39 6c\nmeaning: \"XXXX\" + CSI 2 K + show-cursor + leave-alt-screen\n```\n\nThen exit immediately. The drain thread can enqueue this as `CtrlMsg::Output`, while the waiter thread, after `wait()`, calls `sink.write(KIND_EXIT)` directly. If the direct write acquires the connection gate first, the observable frame order is `Exit` then `Output`. `serve_attach` turns Exit into a FINished attach record and returns; rc returns on that record, so the final output bytes are never rendered. Capture broker IPC frame kinds to falsify/confirm. The existing env-injection integration test already observes this ordering and compensates by continuing to drain after Exit.\n\n### 3. rc omits final VT restoration even without the broker race\n\nFeed rc a final rendered chunk containing:\n\n```text\nESC[?1049h ESC[?25l ESC[31m DIRTY\n```\n\nThen produce any final outcome—operator `Ctrl-B d`, `AttachRecord::Exit`, displacement, first-event stall, reconnect give-up, or fatal read/write error. With `STATUS_ROW_ENABLED == false`, capture bytes written after the pump ends. The current output is only CRLF plus the bracketed status prose. It contains none of:\n\n```text\nESC[0m       reset SGR\nESC[?25h    show cursor\nESC[r       reset scroll margins\nESC[?1049l  leave alternate screen\n```\n\nThat is deterministic and independent of OS transport.\n\n### 4. Cold repaint loses the child’s scroll-region mode\n\nFeed `ScreenGrid`:\n\n```text\nESC[?1049h ESC[2;23r ESC[23;1H bottom\n```\n\nThe grid records `scroll_top=1`, `scroll_bottom=22`, but `render_repaint()` contains no `ESC[2;23r`. Apply the repaint to a clean terminal, then feed a raw LF/IND at the bottom margin. The server model scrolls rows 2–23 while the client uses its default full-screen margin, producing divergent cell placement/stale rows. A unit assertion that repaint contains the tracked DECSTBM sequence is the deterministic seam.\n\n### 5. Picker’s distinct diff desynchronization\n\nWhile picker is on `ConfirmPurge`, `cmd_endpoint_purge(id, true, false)` writes `PURGED:<id>\\n` to stderr inside the active alternate screen. Ratatui’s previous `Buffer` is unchanged. The model removes the row and draws again; ratatui emits only cells differing from its previous buffer, so unchanged spaces are not rewritten over the externally written glyphs. A stateful recording/TestBackend that allows an external display mutation between two `Terminal::draw` calls reproduces this exactly. It shares neither `ScreenGrid` nor rc’s output pump.\n\n## Shared versus distinct causes\n\n- **No common renderer/diff/filter:** picker = ratatui local buffer diff; broker cold attach = `spt-term::ScreenGrid` full synthesized repaint; rc live attach = raw `write_all`.\n- **Picker cause:** out-of-band stderr write invalidates ratatui’s assumed previous screen, so its correct diff omits unchanged blank cells.\n- **Broker/rc cause:** output-vs-Exit is cross-thread/out-of-queue ordering, followed by immediate termination; final VT state restoration is absent client-side. Cold repaint also omits tracked DECSTBM state.\n- **Common abstract failure only:** both allow the physical terminal to be mutated or terminated outside the state owner’s ordered render lifecycle. This is architectural similarity, not shared code.\n\n## Fix suggestions, ordered by source correctness\n\n1. **Make session drain the terminal-event sequencer.** Do not have the exit waiter direct-write Exit through `all_sinks`. Wait until the PTY drain has observed EOF/completion, then enqueue Exit behind every prior Output on each controller/viewer writer queue. The drain/output queue must be FIFO authority for both Output and terminal Exit. A mere mutex is insufficient: the existing physical-connection gate serializes individual writes but does not impose producer order.\n2. **Add an explicit final-output watermark if protocol-level defense is desired.** Extend Exit additively with `after_seq`/final sequence. `serve_attach` and rc should not terminate until the render cursor has reached it. This guards future cross-thread reorder, but it is secondary to fixing broker source ordering.\n3. **Unconditionally restore the operator terminal in rc via RAII.** Separate OS raw/console-mode restoration from VT display restoration. Before printing any final prose, emit/reset through a display guard: SGR reset, full scroll-region reset, cursor show, and leave alternate screen (prefer crossterm commands where exact semantics are portable). Run on every return/error/panic-safe drop path, not only when the disabled `StatusRow` exists. Preserve the current Windows output-mode restoration after the cleanup bytes have been emitted while VT processing is still enabled.\n4. **Replay all render-affecting tracked modes in `ScreenGrid::render_repaint`.** At minimum emit tracked DECSTBM before the final cursor placement. If the grid later tracks DECOM/autowrap/tab stops, they must be emitted too. Add a stateful emulator contract test: dirty initial screen + repaint + one subsequent raw frame must match the server grid, not merely substring-test repaint text.\n5. **Fix picker independently.** Never run a stderr-writing command while ratatui owns the alternate screen. Either make purge return structured diagnostics without writing, temporarily restore/suspend the terminal before calling it and re-enter with a fresh `Terminal`, or force a backend clear/reset of ratatui’s previous buffer before the next draw. The strongest test mutates backend display state out of band between draws and asserts the next frame reconstructs the complete model.\n6. **Regression coverage:** add (a) broker integration asserting final whitespace/EL/leave-alt Output precedes Exit, (b) rc sink capture asserting teardown bytes on every `PumpEnd` and error class, (c) `ScreenGrid` scroll-margin round-trip, and (d) picker out-of-band mutation recovery. Existing isolated view snapshots cannot catch a previous-buffer desync."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":4110,"output":6249,"cacheRead":207360,"cacheWrite":0,"totalTokens":217719,"reasoningTokens":2422,"cost":{"input":0.020550000000000002,"output":0.18747,"cacheRead":0.10368,"cacheWrite":0,"total":0.3117}},"stopReason":"toolUse","timestamp":1784365581672,"responseId":"resp_0ce5441d29b9f05d016a5b420de2c4819b8144bccb3976b829","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0ce5441d29b9f05d016a5b421097d0819bbc3c0aef5ea5990f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0Igu_ucKshQTLNhUCNUp3Nf3YG9OrDGgzoUOFCreqW-S5p0BBENmIDARTGadL3zsCfxRscE7X8oveKU9VcKKpYaqOF2fCETQeCMNIah2ESdgeZdc3RwEgZm6r_I10Ux49MEB7AUy80sZDEwtmYcr0s9rDDLvQwDx90d4rYr9KkZ5GoORK0ObWH-REkBJGWalszoJpfaVCSPgYFRBOM9yWU1TCLTrxl20EaTWrpgP2pClQ8PNnWWd7TOttD9sJbeu8gqbyktAWoL5OK8UFdD2gYgUpnto_j8EbF_ZZmN_YaMO8YrlywH6nCRy0KQ9o3ldBHM0vAQ52K4yhP4elS_nBK1vmHJL1w4yoWPO9LLNty-KPdUzIWtV0f4DEEsnuvXMCcIZOK3xXUmL5vBflFkLfBhcwujudlxSTU8ESDWN9y5F-bZoX9GIOeemXoe-CECBCdnKqc7niINv01TT_zHl6yZ7CMpFak7H1zlOpim_ITvLTTXw5vZTd_wVq0aZNMGRA9rYeR2G74WO-SFYjMzK9dI6gIn6-sppG7fhy30KZfz1wbIH-1akrZQADdv7vfny1jw2ANnRfVXiPQqWLOVJADYfN7DX0sebNXj9lAFfGgp7bDPjA63aIdioLkYCQCiqQpT_lrd6LAbBSFZEgtmJPI2hlU79lwm3cn6pmsMMYI-B2ZE3bV6hp1Dyt8xZXSMwtgnmJNZJGu-a4-XrsxJd7_aDTUHdMVAiRU3n0H3v8msgT-tYt5v5SVaO8Ac_n-BUzvVn98Kh6RB24Wl3yj1QFnri99fPt7ARmbnZErSV_HB8-AzNy2IH-pIf1eGAX0mRFcFFNKOwwOX6SZvx5tSWdu-O1mEOzmGzBgDvbdR3ufdaLQpKivSJ6zpYh9npik3yiVrXK6KSSXiRl8qOQtJ4EpX6JZCK8vFuQO6AHkE1Brv5cgbKh3OSil6yTJ3EtSqwqwC59weSKf8_l_I5I4wvfMEex4sKYHHb13CvTDOmbqDOFXbnEM_G6oM78NNpI7D0lHF5N8IDNGPLgfJ6PzwwglBaFajM_uThq4GCAxptPCSv9g21Hlxl2K5TRLn5nkTiHnfgO1o-bTfNxDX6bj7sA5Gpu0vNegsjTDcdt5Ioe4hMC_27itJ7VNJjKjOvbaorrpLJ88qYAjFcuJuD0lWTCvOadLWz2k-VOnijGke5M5XpqU22Wpdw23ulJSDAplHekczj0wIn7MFcrJ7t5rl8sVQVQOd0SHRIqFrMW9_uzpd1QIDFnqm8BCtXDvjPET10Imcefn6ZfBWq7H5n9o-CvBlM2kgfZy2m0yw1U5rIBbnC8Qx3r3R2UNv8nngvL3OD1w1XoguiLRs76-LQSvttLf6623R6my96p3TzIL45qnLOHKxCp4EurgUkXmR92a9KmQinBENFVFq-0fzbkChj55Lk5b3NwCihvMypFHjNF4cLulOHie28WHcVCv3WQQcJurTh8tGGykbHFc-GTwQGhmhfmEDsH7CD7o-yeZEPO6ZZ-FWYG2GTrf8lDlXOSXWl-qxmw_9upF0pri2zToMIWVFqim4_eZsH2u9-sVTFFEJbJiceKrDrzMZ9knEKuuZL4kKPeKdGxq5sMSxkQdIn9rwRQnw0i-T7Y8vYQY-ewskZG1ZieQ8B0PqncN8zAYUqYApjdB23xBPxc7g5M752kDRYEuBllCvrl7SA1p-xzJvfSPuaH0OsxH1NDLUIXUytHBlNkwLyccNvERANLMkz49yXjJGHwE3e13an32WbC1Z-PMv8bnkrJzAFFSPOrCxx5QnoZdE1CdON31-eky2_xTUMBA1AwOm0YusCUn0usQQLYbycWZaPbv-LkLqu7YzyAVEwElKyFMBEfUzpnCJ5mUPF7uSpxutIvezEAMd3ECFmShN0KDHe9YCsjZ5eN2WWXW2gs58oFcGwygQcA5orX_ptkTEacb-sV5nU2cUOUWyCT5UegQlfQOYJ0cViGfD5X-iKFO_jCib6590eb-eBSEwbzGnLGEKOtZNQINEWB7JK1kKP2ZBs3sUDNl_4aJx-xCdKZ5yhODgZbwiMS-W8nZvY1qhFT8xUJLjKkKhcTtCSxscOTwpLzFvKwJlxBgffZ0GAtVLf0zZgc1_ZrNDopF7rYZr6fdyyIZz7iwsiEIh6sEptFRjrY7RQl8BavuOvkHuCQL412aZvAarqqs99k2M3uLZHWbnToaSbWFsAo8PakuM0mtSmZRmLnjHAuTOnt1zte6b1ccSF8vcPoeg_bQsKFwmDEOoRH_i1CaSdOMrT2Y7MXp47pMcpAFtz1RZOr-Xi1RhRM8dZfMuzZ7UqtOQ0vD8s3MHzsZmAMeIT7epnd1V7Eww5n79IeP3UJ4zrDlkcV2Ca3-kRJN6lXS8jOGonhCcLlXyPFrr5HdIXrBYeIrJn6x-YLivxXoyEgd94Jwq1kr7J9RPj65udt_xUDp6gg_zlaJE-qsSOFPfPserGQAZdSoMEmd7C93Cz3fYQknI4yb3U3_cO8TJAfLYmlmauad77ayW2Ah_BoVzY2RzipNI6Q76QsxmCx8tLNO90JPyu7eNtX9rXJ2xnUFAsElNudfmin8SYFBXyqDJfxyBafFmLmxTIuSeCka5lDmqy8Whv7vRrm3c3tFYckHVbX26D25qmr3iBS7suTaRVYsNKsDfX-9hhkTKdrqBwwYjfDfAsobm0tIsH4Lx5V1Hb1e9aXsBlwg8azM5CIawMoi8BPwOicsW4-hXsI9-LBhWeWAqKPh-_CuO2dsadosRfWKHU8mlU5YEaTjqoWkqTV8kjtPjEXaQ0Sa5MVe47YIIGPdfbeIHziyh2-r-IYGM9RhBknTUc3T0deSsIYBLzrVhykyQMqOZfaKbinD2JyiAfzqRNBVa2C7jlg5UgunaVT-Os2GPXtVRHGKNAoMOIuTkhXTRqJZtNW0GdsI7UaehEHq_2HlSE_GCxAO2rlfZL8XBVO75BDyl4q1GdJZCiU-LaizPZ92DM1uehI26yBjIeldE3ctZLjOXvfQflurPcKvL2l-S9fszAGKTPGOL-fYGqpwyFrxSKZwAVW9w1jQeIH66kwGJoSbPPvqt17JLOnYBH8LWMQPpWGs69bv33ydzsJhonAxfTwq8K3NzXp_xjXQAOGbsyD5NiuHiN3axk-jHM-s1pK13svEabTFZCRLkrfwfl1HLk24hi641OHxOT5_oZkP_8bxAqCCHbLKrjfFEEWW5kDJUxJP696iuWxob7XiIXwbOTRdn5NZJ93zdADYHjYKT7BdC4o9t6NpFsdI_7gh_m3VurmP38bi9aDGWdxtjGfzsW41J0Pg4TMyy-jevm8TeF5FkVWhKA4gYvKBHlGKmbz4QTPKq3nP-rqCzdtF8v46Tsz7GYi4HdzC5RQ9UmcJ0evR0Jt_tQ_uH-KFJSvYF21RSdMHWhnBl7KvzESHUjGjMPTzutu782PITvOLJRXgFiV8Kf7H7V4NlL0S1Ogk_kPLSXmwqSL9GL8vhUKtNnO_ateCrevSMQDzLiKCutQeCSfX1new-xOvwwPCU5dohOZm53zMKaOG-Mbuz0h-05jN-_ptiW_J9C544Yb4GijkEX7PGxT605WJcX_WfZlAcn0-b2uC5VAy3G8_cHeWltPY2Hcd9X6RKehGszQaYi_K834S15JmVI5xS2D2KOfA-JOyx16VCntpYJT0VtNC1gpl-lS5Mq4feUwiepCZ2jqS3uAFvjx-kA6aQ_z700TWAfeIun1xDlFZBpq-CmJ-1u3jWnVvzfMEeaMjgcvUyatB8HUEGgVpFfBh3WfVlmGlUnp6EApEuBXfIt-7w8WqV4g9jZdmH3AxsaTkOS1GF_LXYiCbDVqN14unJYqyejUZNh19KsRZtelyrx5RvsJdB1pDjxq8rVrV6Ymw0S1Juv6Cvo4QaVzYD32HijlyIDZFQ3wIMgJy-VIxMXq-W39JVx6ovGER3a6ZDTiq4ylem2PLepolQXP-ocHr27CYYSm1B-tM-ABszoKeqeARPLIibOPCQrDidDyAguCF9wEZE_OnZ63yLIRm1jtC5mLSBP4X61zdZgl1QA4Cqd7NvmmDpm8-g3VJKExOxk1UHgpB48oRvZkPA6k-RxMiyaWOSqi0xyp7An6UvlQ7Hx0poQz5ENMt6Q_Qwgl5pLT79-ngCU7MJEam-y003wJe97yY5ScgLLR0lV_hICmEZjbu6b7fKtqnTsCDGEld22BsEHGA23B_w0YMYT795IqBquH6SPwup5sqVNhodC-RgzkNQtP6dHppwv7jMkeeoOMkOFmFxyAOBCIQpJuhE8U8do1EMhpBo8u72HdxxD5qk73zrBJNKrhHMa5PmBqQXl78bSBGsICMwC4uaJANLaIOWB7uK91kcTxxqh11XnvXZIhdjl0M=","summary":[{"type":"summary_text","text":"**Analyzing connection timeout handling**"},{"type":"summary_text","text":"**Identifying scroll region restoration gaps**"},{"type":"summary_text","text":"**Fixing scroll region reset on repaint**"}]},{"id":"rs_0ce5441d29b9f05d016a5b4220b07c819b848476df6491cdb3","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0IriIqEzXydSlm-i7eObk_4iW13HA9NfU-J46W8AHmRW6_mazGaR2s3PuEP-wQo4Axz8A_ccaKQt9cQR_g7yTH8oTWM0xGsY4h1UFJZ5lbh6L3_848r9On5nknc_9lMTplb4XtYk2nmuC1MsnI00sHQffDCUYiZQm6Ieq0oXng8mFtwWg2tkY3RZNIY4XN0lIgbAnPDQHkTHQrEinkoguL7BaCneXHzi5CsCmGZ1GKqDHbglzR1RPI1eTxt7v64UWUkHdptfcUPp9STuYtQtpjUyFcXN8f7adDi_GGLmAUHTtV-CHOMy2hTx57cuvYUeDJdN2haJW8xtlveCpgxaa0K8ZXOP5Dv5w848DNJNa0lANFWRe5_N9N3KsfNfWkYGi1ibRHpmEwXnG8GZcXi403ifSJTzJz_3dHzybd1BBu2jax5RQjwnwv7oQZn1A3m8lufARPjzixbv2lJRwK1XQYgm5tGogKzTiPsMJEDT_6CJ3Gkj5_5uqQMOdmEEuw5k8jX8mLwQxZiQIXpTqofZnGz39ifU6wnPEM2ZYWno85P4SytqV0CX_DK5mE49cJVvc-e5iCi1m5GkQmKmqbptWAgJ9s9pIbAfZ6cB82xPdCtiCc0KgyKlqHvA3N0QVX_WUc-6R2vbJuiUca2g0_aYWPv04B_YJnLpFeoC765PQvhWW1b0-L1nSkpQpurHgLozITQHFCihhPI_ynKvdLWY5i5OpIuFc0YxIscMw7wZPs-FThqdx8rp0aBQI21TJIpPGD6QA4j9PqrlT9URTs-ho2XtHJrUEmNpNntbQhl_P5bguUI9v2_O95ujB4NBRefniL4aKAMiti5xtUhX9X8efd6YuA6eAm47seQON4-SBMtmcHHJOn-Ogb1ndazCtnwk8jUFvN5L7yaA-DPzo0kjs3wlNTuPLc1cib2_xe8711GCqoLScEMiR_CI65g7anEpuXaERGZa2nWE9lswxDV0oATX0AFJ9rCuQWE6cZHoHJXMfS7ujz1NPA8UOBlca21sbfGzJJPvhsjsV0QW_QAL2ZQEEcyorhiQiFrh_gT_5FKvyA0G_sXvEXqTJx1WO3FdLoD3vuMijyJuPdG80esuI9kgx71XUGy98rkVYkr5J0kk79mXn6ikDbv-vgDdODullw2puJHczziZTC81Hbe5ZaxnWFA_3ktr1GuwSwSb_v0uZ1Oh0JCCdEes9jePo6UGIvsuQdjBSPmYFC2El4ALLbyMpNSheMME0ID8EIkeGIRgfij6F4QRGAMloLq-LpMVR_U9H-aWpeKLcEQmuDgQX8Q3diqW0F7uit9SId3njRZX7vkCRAkjwoY-hTWfYHgWl3hAbGqaZhSNNV3qitsNrZOGJynK7WKoQcXC7kezgPR1mXLybeKHBvKXQH9QJXRXDpunL6Awy8vJusn_a4u2ps0ZuTNCG8EgaXyRubWDWa2qUExvwDPwf-HT2tw9AAokl2dG6cdosoJMTBXpTWkOhlhRe4uTFZLuz52GcbpDTmmOD9GC0lMCdWCgOTiOf44SUajjD0SttCH75tMWRlwGrGnGgJbO079cD-y23Wv3bjc3EB-kbH3QxlzwrbBfDU4g3SKQUpGuamDrnaGsTiaLWr0nHGij3897QOlKb7h7wGZg7HeFbtdookTYipiuPRj1K7fQeNhi0Ws0Inm3IQ7A6JchrBCOOfKw6FbecSrnwFsptok1ASo38X1cIzXCnP3kPX0ThfShlgZNetkOhqj9c7PWrdD_6z776mEx-MtmLRtly0wQAawGksTd3AGrg_67ALJ6RKPoHIT2hnJ3hkbGBsNsRfjH3znyx7cj57l4wUgyPX8BCWVCdCwjlL_SN1JBeuBgqb0_KQ2J-igZng-ClM7Ww12ayOuCdOJNpnTgjpY2MgfXI3PKPLzku3ajgoiPkV8f5toc0yYyBalIBgPxK4B2y9YhPeXOXITF-ukcNh1C-sIzOTmrPwDuwXItdsdJ_d5dkjHi-BPzamV7UopMz9wwUlOFlII_xxAXNEV2E41_yAbtuevcQgPW_UZgG-eBsc2WWY_FKSt38GAPRBtCDT515s0pxQ1qfw7MrX36XAVGZcxX7d11ROJ-rizqA9O89An67JBL3yEATHU5Z5RyoY7rgrZZT7cdTSHv31uBhZFOghu2i7p-69famNWf7L5ebj_ABLWS5apWQ131A3DRfigpb_mWWVmNU7rrIUuN5E9IZg4uhgcxvA9Wb2IDwJgvFC5PpngYIweP9gYLv9fpBn4SbETj-JJcKEGlLd9Rlohe5tpPXWNPAZFBWCBWuqZZ3kRW65w-mncx_KN07OKI-ztwJiV75VCv3zqoBvMOCKtYnwUgWAoO8gl3cZ9cXGF8hWeHD_cqSAAlttT5Qi3VcAhnGLXw2fU5IbTKPcu8J_9VJ1wCvjSkmejRDZb8ZvLsk_lkt3SbM-ex-v_Iva0j0Xz2ONNTAZBWWz-qRoS_-99UIrjW-pcAttIXTqAbxPwjn5GA_9_pscLdz5VKNF1plydzq7poeh2zcmOKGVYUg5ypSXnR-yAHTlNmmVygq4DGIzbnbc_WokGu-mfiwWrDz3MvU63rBFCaOqB69K3hyMUup5YW3lK2FberDk6J9IV6Yl-aiwDfNn8F-xRIi8nlMuSm2NXd7RAzdKT9K4CgA9oQBMZDkxdYRNkkpbhGv4u3cs3HdZQ7CGSly4ziyG_PdG04nDdtxaahMEitts9jaNj6Z1QGRUSIpKYDtlcnBuPln5CJtAYn--qSewQU1MYnoLAHRKL_CGyMIVGHHqhsLaIF5j0ezf96hrOxMlkiY67AmYhExIYuVDTolcUAj9C-J_MJQwlcCUnn8CVirshOGfLKsvibGGYTyjqNnFG-gBNmeLJP73W9mUW8Fo9zzOhly6BH9MvxeHupOGRvDOTMBy6kbovw9Z_aqteygwAP5vJqejsGIoHAOC4HdqqXY475KjO48BAdTG5BEqiR7Dudulff9xQscolmZlDZzKEnDRiCmoL_Xlp-7ZDoj3AQd5iyI31zroMlSZSfDln0DFB3uWdHVmIENJq0cfDQjCTZvKENtOXiwluE-iKj_jkMDB6v-RqbzrE5-JZQ_gy79sKasL85z00Pi4vvqC8vw-EepZ9FVsiyDiKkB7s_Rt5wK6fG-VHWSk8O8go4q7uNWWUio8DvYl1LvE7SpB42mwOtjoMEuj_84WObNzp51yLkOjN9z1usIYQs_NQkE_6-bfIojRZ-meNHrryOZHE66moC-7KbRWZDELxk7cixVtX1uTeUcfAK56mJXx2nD5tHQiB-ib93mpiuDcv-Bqi9OljVM_F4LYL5kGOt9OF9bwZ413sH_5uqvZcS9drt-PYcH4ifzhVri74fr12yWb0KvsBVCA-qooys-j9hdHLqwfUreq3jLTxL3m3IT0wuS0gCHYrkXaJ9y1xw0bzZygdEgRneFotXHJ57G3JkqMPwUWkFWJ0dESHOnZfTcBTFXF3IHQEtnLEwh5unN5uiHH_3U5gFWstURAoe557KuPc0JbRo9V8YqkpmHjgDvPhKyWqlhX263sUqosjv75OKUyOwjrsGbCumw7I52XzXSlD0PX2-EuJd8d3BBNZofh9l08irE597zSvj-Sykhq9xu36FVvjqUmmQdw8tNXkOtDX-V71UUUuLEm83_PNMXq5FGKJkZZWDE2AAX5LZmPn2VSuasqWtzT2-Tl8DtdBjFhGxqd9uvn6qbhOE1M8gMY32ruAqtsuQsoMWPEkFCeuSl2WukAO2Z306B2qKqpazW7SnJuLQ-znFm0G19YP3jzmUrMG-dJoDUtlc_UTqEDEi6BDC4FYyETA_jjsfEkq8XclD2CQx4hf-O4yUsDuk5AvhNYBPvK4AdFaFEEmmRDCHXI0jYo41KDQw-AEG6qpynI8fqtGjVPU-rF0mmVd1ebxwhn7HxECMApI6zAoasqoN5P7qEL201RMCsOypzHdKsiEY0t3aO9V89yIkzRkzMfPLGh_rPzDXygRHpClnSmuK_eME_WhbWSOvuqxUd_FR9IOG6PWS4TEZRYb0L0rzTjyComDfw4jixoboNi4NwR8YM9OIxrhjRNIoF7buEMJj8B617cw3xCqr_0kJKgL28y8Q0Zk7168SkAft6JG3sl77sFHR3mJUmVDfLmbuL1YWxuxGS6eIZ2XisgnS1j9YOIiafBB4wFLnOOeV_f42t9jpEiARlgikyhA3VMrwKN3okcQIMyeQ7kewbcrQ7x98W-zWi7hkV6PwfR2Y_6sEQSANSxL8mvRcsNGFIH345yPZlrvWdYPTBkZb6muyQ==","summary":[{"type":"summary_text","text":"**Fixing cursor handling with DECSTBM**"},{"type":"summary_text","text":"**Proposing deterministic repaint and teardown tests**"},{"type":"summary_text","text":"**Identifying broker exit race causing missing whitespace writes**"}]},{"id":"rs_0ce5441d29b9f05d016a5b422bc5e0819bbd457fca46eb8add","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0I8XAUK8TS5Kcvr089yjjBuMtrnoRtMwyuKWyaUrxxyQvW3TzubdSPqQLXqy3jUUqL9KV0iJz76Zi5p8cfcVEjl5jO0t8wd4qPcuAViODXOz12G8oU7J4ejnBgIE9LEU24z3RByUS_s-biNWkUJVYFNXQLWGkvyt9bvr1UU7hWdqvZayj8U7-wY5yA3FdgHSiN3VRNcWjNaWdCWzTYVGmqZyLGV_5ycNEDpHFTzW60HXHdK14zicg96bxPanEeK4wu3pMbeqDlE_QTlU8o_ILQsxdcKAuFwzr0iYIE2JmSq11YpewgI3nIwyDc_ERma1ZIBuOQ7_YsyvSTqnEjF3hAAuw35F9JT5l8bMOwnqBVlg2wL6cFAVDQwAkbo4My6wULLHKDXTfKd5XD4J-K3D4haESG5NxKFluGt-Ykdfi9Nw3atJkUiqTEfUWLRN0fohtdrudT4GaVYTXF__OddDkZx6Os-3cv2GzNaInNvSwjboKruRTDCvg5UOoKMUXiwt8By-yzowKeM62H--Nrcs00Wuzuz7y0H4vX8zsJJh3UlVtKtwqT2gfGhrrn-YWQvSN922v3X82YVTsl59q-HKwQI78DxvAISfliVzncRbxbe4Dd_RPn3_2lNA9fNg9JrRbeJvnlN-a-4Kj69tlwVMN_3B_DvjbUeMqkRxCC0GdiSIDKHbVPjuGDb4hzbsxmMW9ss2fE2Dyc4Js4MijQAkcvl-ld8Kqfmcxiyqj3rapj7RPPwldvsYoNBJL43KehoF6U45QL6jv71cOFjPjhr7WC6ZWg6FhL2l20ClzqHiXocDJwMWIYA9LpmDucoyRUcLOqibwu03g2seupLxKHLeouwg7RPFdK0ia-kPvr6Qg1-3zjhfpZBCXETPgkq7eypWArH1zIOGsxz2HrXYuQLWt_qd5J-tb9Gk7vlVpP9oOtvUulPCARhkvZTyCifIUbiRsnNU-V8zd1aV_E428W2NIVNcchExalOIRr-jty4RiEwLN1KdmVn7EUmV1sUOPZTP1DXcu_4Ji2md2LbF0FTdFGJnQ82xUro0lenMTCsE4jTIv2nzOrheCQ46ivU42IVlUd7zvAdWqmxq7sQ9l7Klwd98nBVdfpkZsvbNOh_TASdo7kP1YFP8sX_ef7fFzLgKFy-ESqooMlQQyQvVSbmwB8Q_0BfcuTYWzYK6kX5WtxmyFKzNLX8LMcLnKU24gqxkkJHjjc2uhqV-cGrFUyF4ybSQXVOPnr470bMtgNk4ujZHgcYQZSWHeOIW4bGGyeU5X0THiESTkBqYxIiKg1WX0LvR6stBNt6R47mlcsMNMnFxRBGVDQAaO8gTeMfHlH4qXkihzRwESK3lAmhO5NpODXqMpu5WlxIN3tfXoc3_4I424UIFHzbgcHpiKMCmnq3PKBXViucRuzCOa15ZDK3Onhu_Y1V0hV-HiwZwtjpou2R4e9R8w908Gnn0ZGuHy4XUq5-0kM9HX0N-L5CibBxl5jHAesqo0oGxX7yC_9NfCeUyeAovgdEZ1FGrqbQ9WgpjhDwXE9Bp8w9Z1O_QyJkkLaJDYAvmZijQ7kkYIhNYMNalkwNuGVtdeUkzeILthtlpQN8gqc-hgGjIMAOg7cCHg9m2NSKLwFwUQ8AxVHx9DYWnC-23V2GWFeV5TKrmCFjXgnamVEOkLyrj3OvLzb2w2pwB7hLv4lQvlHa3bZge3Sns_sQObb4P1tu5s4gj20r-CPOnuDN-haA9CvUwigvkRcSDLAEhOonVKJp_ARH4jJKJja-trvMOrVgKXjKDLyMso5dGxjDRNE3FL5LeEhBt4UtmcLYTYjsj1B2ZuctrWWmsEYBP5qSeS6avi5sE1hjKNBml4PgjHGqiargu5N2EXw3TjunMZpojOmsmdacmV1n_Lu2T1o0YagyDIdxMjzp8VRbN2O1Ho5vAS8I_G7Ac2jBSnqbG04X0PNPLRcfMf56G_8-5RKwg9oYldknWNBzJwDoN4u5gC5AC7QsYWzh9n52iNnOlKrtF6jI2i-6vppr8JVDUmRbxyb2uU0WzO7RtDzT68shnc2a8GG90no5SYtV61K2mvcoFRxy7RoQmGvYo1NKJEo69etghFuLz3r7VPLwooJubGb_p7tNnGPRh3o8VnJEVg4YgFwqXWUh9CNnG-JDDUZ9_gjogk1OndnxPsoQKp0jnnLWgCqmKFJkbVbqjXYPRXfCh0ixe_b-bjgnZIGXkIufPPBjV0pIC9WiGrwyJJkvkZG-HZIZlSEN-Kq76vTU5qqnxyxunBPWiy5ChtC7lXmVY4dkhS0bJRYphz9fI-0JL0yNv7uFdtPQyShq_UNgWOduLOBzx70kBg6lq98EvbETMFdwitNlItWLkaj872HDqWb_WwmkiNe1lPvd4GAfHFwoLM4E3QOWTJoBP7NIF7b4_x0MFwCWvy_ZpLeufB5Ew6Ic3C5zDwZHfHArRAq1yQn7MgKMaXeFlkQ9nJJWae5nIR7jBoxKpimiEAWQFXIW6QxTct4WKr-Im_Iqbtk5icHvtc1oK25xCw1vCmTGvDs8GKeq_1Ct5hE0_aAujIPSAtgynlbKMF13jFS2oUgfwKsKePsAdoe3QFyC9SjmnE0WaNnfSa-I6ACVneJoYyec7Sx_NnQ5x9DAQCL45Qvv1eGlcxUUWbvU4ilQXpvcdLXJ5fvC8vg7R19RcZwWRBmMAtwwJ19Xy9Q_MPpwMIZNeH6J-t4oUhLd5AVchtVWqlptyn89D99WRyON2u58PxDYg-r2tMxh8sxP_vDok8Zwb8A_AleYcuVH3y4W7LXYQE7AFUm4ciUycDlo0L8Kx50Z8UizYpfh6tkYlkGcsEm_JPz0B7IuB0Erc97h4x2iIBRxhObbXiOgpYZlMM3_SasjvtoXyLORFN9_seUn_D36hBhBbDVoTEwzMe5UmB9onFuWJZ54xMvxCQeYCDKI3MdrMgjWGFz7rR75x8kn_C4eDEwTyzvoDpX8Rs9HHe6Tmoex9IVTDgrIIIvXmc1BqwzEyi6WPbV3iwvDs8Jq23xKWNoOikecmliJd6Zu7UdxkhZn5V_6diXpqL-QYgZh_KyKr7H2lokLpg6yzdSs_ZSx_q0Na54g5QQRUvtVzzK4omEax5C4XjEBxHRuNvu-aMMM_FNZMpK44Z0XD14jQBiYt7RdGlzk-zhzNpgbNnD-E1lZ8UF9nnXgQKsPUAw3Qfq5z6W7v6XYWRt5Y9uvvJO7GcwiLI-9-jjg29WmVKwQTcfAasmLDsJ4FMoiw5PqROLuar0gfTTxafUcj6GcDyLKYyhNXmzpZPMq4hfeXOe9PqiuKtUfplhv67aowRoAiCGjXaA5qj65CyNcz5hjykbWuzMIGIS4qP5PhSOO5LotJDQT1ppvKDkCHN8oK1XdfxMqTNl9z6AgTpFhFKtCnVGGunpZudUc8Eapct6Yw7XtT80hu7QMuhdOpUw85gbhT83FjG9fxS3v9SWgj0DsJFQSKG0G_X_tvusTUKz0KSp64DhPhclx1WVm_XEobYFZtbtSs4L5gj9lyDbZ7abH6befcIMf6-QXAD7KHxSDfHoubuh70lAy5rJzYte29IeH3Kr24ZTpT5FjB2weihZQAWqTOi1HVRTno4681VGuliTg9VJnUNc1MU7_OnY4A3iM58c8WXMFPOSxBGnwqsKuFZdEFvZEDKds6OQ_jPikP57DGqHOl6bnZiFNdkapYXrD0tbA6T7G_x3HhB_AyEFU9-Bz6Wn2reiPfiDZrZzAzR1FsY5LHWaUVWIbPNYzj8FrhVuU2pYCTdz638au6jFPKZ3un4r-4AuGfULu-WqVATsFKaSDajELovtLrnEeDJXDpkE1tbXxFgyg3DNB2lboAhFpdv8K6q7Gdysgz2qmrzX33f1MMkAUTRWi-GMRMzwOky_aw1xNd0s_RR9APd6aT3-LHAhqgz0cZ6ToULipkG1_TZ4zgAHwpFf6I8G1w8XRrWmEoxDYFJj1gmJbnmD4tkjj60xWRtaqh6rOudVodGYHG85tUYWgnrQbOicta2CwCKt1MFiP2zvxAkSZpuDvDobPLFUxgbkLMHVwClWW7NbcPlaaSlzC9RLxY_OTR3rRAfC2KS1rcysAoIgleTELP_4XVzkYLza7PT___f-8RK4iFsIH-aUSvGquYs_itDpT44fwopeUATVl9QCJEumfNxcJP9NEEQ5c6TrgzIqxjliSIusuOyRAkdHAEFapJeHHo2qisaf1TUMhj526WTTY3dIvSCttFMQ76cww5ru2R2zb65ESrponnciXwOQIm54tP3Oj0EtzLO8su0vXQ5gIXVbC788AYEWXH75quC-qMt-2Ef8F7o-zlMvykY_iCEpXY1UZ0XNRV2dcrbjW7d60ow==","summary":[{"type":"summary_text","text":"**Planning orderly exit write enqueue**"},{"type":"summary_text","text":"**Designing coordinated drain completion signaling**"},{"type":"summary_text","text":"**Clarifying report architecture and schema**"}]},{"id":"rs_0ce5441d29b9f05d016a5b423c6c78819b8ecb333d926ec07f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0JFejhHoq8c_j-llzPFJK0PtPHHSxcA6GVAPvxggJ9tD_JOhn1989cB9hFHzYsGLB6bq8oxoUj1Wk_J6urV8RL8-0V2tEO4JRdrotHFlWvXEIRMSeipGJXLWg1jSTJ-vpewOsPbOgX-Qij5aAO3WPZTLPj2UcIZBttgK1Wdw6F3HAQoZWvRYSDth0gDLoWyteGDN6GmPaIk0ympHoTRaDk_85N50YKXgPutlJH8dYSCPuY7KGsqS25NjHWj99XYBrNMzCa3wZ9BY3OoK65cg_8Io8ZEkf9eSf4ThxnZTGGR9GRHPtOxz9_Dj6tSjb1tY0vb2lAaLmnUFem7IbNG-c2El78nFj2HAvnet6EqSLO_7DmoPLCkURNLxJkZOXmd5LSGWIaF2-sTg6_eXbrcr1nYgTrYVS8vTBRaJSOey8NpGSnQAD2vakh1w3xBsqNmFV74Posn9JpY-qJMHNBPqRVBj7FxEryKKtVnvNNAkPZOBm8HfdWoEo0VsJeSu7b2UM2U1zF4T5zEQmBBbH15oEy4mA9Q1zQNHutqB1iKRAmVyjcrkDWlPu3DyFABQK_pyh04Pxg91kbgGnWQxLV3FwPx0QbD1y0ZPBff0j2puJ5iA2gmF7jCDlyeprc7DsU25V_a2-rIAA0dcGIPbHgv6FeUmmgBuG_25DiZekALQYI81krQtRxa3_qHbqJfqx9DVkicxlzdosh0DfTZFP_dt3f2vvU2iJYjAwRTugUKZagygf3E8SxpQn3TI-fAa_f4f_lRxH9eV8aWbLUxXmX6Rete-kxfLxZYsthvo8mNNcebTgrsAkOc_7MKiPruDWGXaM3cYuNETmfw-GKFgjKUMBlD0vrTgrct5cgcS4Jb2EFtviPab2JPEKhM0r28juJkd87eC3Kwg5VTD6oLNAT6o2DCj_xs7Yt8M2MZtw5SBLU7yYBrGOtdnEt8WKFKkx6N4bIV5iolhB3an7brzvxIpf1VvHxg31e311KHWXOar5M-YHAIfYoJ0rO4ESU0P1RnQitt6VAZT9qTAUpOrcHNZRFN6Ls_OTKbl4bLZNpBvB87tFFuA9a09FB8qF_27DDbAWvKQlryQuRFf-buAXwyjhM6L6Ax4k3Hap1EPpGLJOYx5NGW5sL0-pHk0KMEyfhiqDv_YjsJ-982R83jPR8pY3VA_9ZJ0M5b_T5rUA0Nh1XNfLV9Ulf_LLO8-I1NI3CJnK2H0ATgCxuwfx04bbBD5ApPkLWkF1X0VMMVP_pzw7PU1WrJ0BKM-HyG-IZ05zv0weyYtY7e13Ty219Hv0ozsXBXvz6n9Gq132zibHIVSRXQeZmjj6EtMPZy4gz8zIKxSOWbuEkCsfsOx9gjEppjsjWzs_hLRMC73Ca_ICtWvN5CU1xjzxfpoMqyEg1yG47zZVNrnW7H-3n6g4iJ8PB11oR2akW6JDdxWfd2SJJCbjHSTuPU4VbfOgMBfVvIFgCTRKGvcg9cVJ-kc5P_oLDM6R9fBXsCx0lq27OeQDkd8KeO6cFORNWUEw2eAndDjUsMwCu0s6VCF5drQaQxoNxxA-U0_ikB8AmB-t385nqxUBuFAy_8qn9AhsWR6RWBPqtHLzvQxLXf9ERbsfnSZW3q99V5_dUk5g6aTgeNEYV-_zgHxsTpf8a9Xf82vgVXBg2ui074QKWIeaYwp9PX8VxMzhdN3Wtz4cohzAepk22RpiLnHZvBFwAaouSlqP1zq8pT0vqWLabyoJ8oU7B6i3zL0WWimEjeUodTz4FGunNou8f21K9LIh7Fy--F6LyG2gldPFUL0mAvZZLYckmlLvVc-aZ89H30mjQ5TNUKbUdY3Neo9TfS2oviyVR3SV2x9pmQrFsWwdigdFKx57KeLgtjIZnlgfXtiOkh0x-mIpX9YKlbT3-MkxYKG4Ag378KRF9tW_eAuh3BLxcNzcx2gkVWgmj9u37i2nhV0PEicT-oesnXNPnf99U3F7OLXO3YSi51QqEFRSCCleyFzLs4RzFetdduGhQ-OCO9kg7g4pX_6uqwvRg1ULor8GrrKDI4a1f2LLUrRtEYT4M4lyg3gmTd9TYxuYItRK6zpMyh6yCcXyIZXaqRv2fIhXj6cVij9u8BD-ewmXeaVjEeOnqzNCAJ0IJouF-l0APttWoUHFPzCFVMRNz2b0vD_CMgrY2Y5VzwWfIZAjICfESiDpWTAUKfklu8u37tNucgKsOHVT-PSXGMnYtOtpQzrEo_w2wN3pwVlruDG3h-YKWDMMUa-80NMVqd58uj9pd_CKiFuX_4ZG8k1OtPQ-FOmarffYFwoKM6AlvbFbyh_AT05nS52a4BEEjJlNgTMOhGGbW-F-G5a5tBiYWR9aqMh3hRpBOjUREgd2_l7revX8Ry10dQGCmL0pfRHL_k_nl84iAB9OeHm-DU19BKp3N8_ftQi98fvuGSD8D5LNrclYWQd1Ys1Kxt7VbverdoEa996ZQtStZ0gPtn1YpK_UpDN473gWWU2GIOQjGrq2mG2KtyHGplf0OmdnmmtMDGFBbNbaKyLrrbyS7pH97QnmReZnod0s0iPTMtlQjdLvWPSro9ALmtWq3bbYNOJpRtiyaho3f3fDGWqFQLNc9imgzJRqsfoSjoo7I9ZXVN8oVkxhKI85qubWT4jrPCwzqpRv0CYbsoeQ3iN84xm6RIdL6AG7Ot5_KrDiIPJ9Bw7Sn2gSs3RPTU4hyCA7biSOI4Zn6xcQGYL7N9uLqW00jXzZASMjPFHFoky8Jx_Fcpg_G05ysT0piIqCtJEJFy0xJ9CJgXN4b38VJo0xiIX65WpfWDF0HA_Kwz5fWnKm3aeOCuqWYzv6hXFPVa15o6CtnerPEo_pkG8mnOcnWRrn8-5ObQUaaHcJojHav64ICv2AWStvtHuLhjVbvHKb3HqW9AYmv8j9mL2lEVfGKMtYPsi1bq53rkcIaCTACqB5j5MQmjeEqdT_YYqX42_QOAeL18WdybDyJ502XsnyY_Rj4sUoF19ZvTJqOWlA7wzf5-95aCr1GSqlYYtJEuEdOIkT5FjBRPShYJDjdLzHrSA_ASJxzsoiB7WaON5f8kiSchVgbOCsdl-c4MhAuN903m21bQoTCbKQgPnXIOnoWFKFl0oXIJR3kcwOhRcyuJfoZ0nV2xoViHk7voT574iGZ9n4MDC5z9RcjOmiDNWKLl84V2ji8hC1KAGqP7uTJRlR5CzAcp9krKgUiZRm5nSgUtUDwBctcQEXfo3V9659uWewUeX46OozzP2iAYMz-qsXNiWHA7aQj0vt1XVQTe-13R0Q==","summary":[{"type":"summary_text","text":"**Mapping function call sequence**"},{"type":"summary_text","text":"**Verifying terminal output byte sequences**"}]},{"id":"rs_0ce5441d29b9f05d016a5b4245e678819ba26eefa1114f1fba","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0JN9pSfId_4Z7dIyaXOWgnREEF_4HtVJl9u79hTC4fW7vbsWx3m9iCBfYir6zVEJtxF_igq-0Kfqi1F8nDW5YOCOO-mspKS9ORCY3mYKHQMm9GYivOHA7wU5uI0o_jKmZfXmmvr3VtSztKcI0Sx5Tv19CDsghFVHcLvfECyaZS4Rpv1MNPyR2SnCw3WbYx0kIU8JqdY4YJAozlcpq4HuIKGz3xwf7pSxJ6ULwdzApHOv3sPbayUjCHze9UVncIMJlvS1ufFHNJcnwiP2XzxSPcsdME-wvg6pOtyarzdzdui6II5ngZtFe9e_vDLsDtAfiiI2OoTdFyu6OBTf9DtaJE6LTwKH4MK8mfsev-OuP6zkbSysJZ7QMa43PYOIrTt58H2Hp9hsul6u76aMra9-DupsA-XzUydo0zWa0rOO_jy1ION_OyM0UbFAAZ7r7Gwmwl3ttjlqAH-CfJjq6ZBzH4tz0sS2E4wCY3t-4zheQB0cVWSdTZcJ4AtN72nXi6mwiBQ6kBo8JqLYe_i738-Rsnn89UBinR2-5eyqBiI5vTDSKj83fu7I0UtTgmHOrHL1GJncwO7cZjU6l3o6u302d3KqYdTBgPBhtAu5PC3uymL4hqEvdHDxqFqE_6y2dIqeeCTTTb-PkZPZJ1VKI7gEuAGihJuEeflJIchyymAUW3ZDU6F4MsFB1_JphBr17K2feML49u5Pu7MjihB7vHO3GOLG7jnX5_3Yde12me-UTVYKMFNrM-P9hjeiu7VHowb0AkGrv4yawn9qSQ6MTDNVe8NiqVkb6Vp1AkJiUNebAoQIuxBTjKza5MDYet6oh1nT3gktCXD_3JWBWFxDOORydnnuT-4eCyk-CjGUBrSzHPs8ALzaFVMC0MZiYtX8NbkU_rEiCAeyOmWcKp4z_nqoZDPmVRAnGexQOhFe-lw5PlulkfI8rMDTTgDGqLmP2EnU1h0uWTsKjwWQsinc_Cj8UunkUAwBAoWZWEcPoIAWIHyV8jtddZxF5NjUrJRjZd0oJXc1c0_rrWcpB5xmnjWy4D1Ksx2GLltC6Nli_0FeLhb-K6nPjYFsosOdeXSTUkqB-ApOK2sIs3CvPShFrL5kiowf_wn9Eycpz9MTgxPRiDiDt__tn37AOIAK5Qrm8TwCnLPdzMW5y-DIDArvTKCJJXoaQU2g01QLBMVB36rc9D6yZWgoZMkkvgEukmG1QlBgozG_Nza8PrlVjANP06bkapoP5NayKGCQZ0ZVqrja4QTSez1Qf92F0Igm44jnjk3QbJ4ROCk7blfO-RJE4snVwACxq9HFUIa3gFtl9k8parlvYMj6kpInryyKkXuiIIPnXEBt8B-FuDHXilwTjJZxtcYAzmc_A09Hn33eaiTkW3GFjZNY2VeHD-pAEJAC1OdYnb7_pnJdOFIRJ4GwanQ4bSd7cn2slz8TYSgLkK0jORz0IMB5sJ-xNTBS2h5pbAE0oipsLqgda3pO0jO6qhfQ0hlLRC4L3PKfjlnGV7_JNdn0tu8l-APxqACvALQa0g9PJq342DysA4V-GRCsd9kILw7wcWlAZVFmSY9iTecpyW8MnVoP7-NVx8kEMv4_cfjzCwcKQ_pN1kUWyYdDt9Yf3lxcpr7rgsGMJVetsueAUW31Em-6pb299rCobkFA1x7gIM-gw2IDzOwIVghlkuQFtlICRF1ZfyUueVZZA-h_E3pPT5Q9Y7PglKAOPAsccN_AFr4J0tXkGBH5jfEtthNfzdzsNKB7U5mkCodABmkigO948bLtSzQk-mBQODApJ4DTlrcBpN5CzpW8fvcLg-WkESHuXh9uF2V9wnQMsCeY_oPetL7m1W6oYe6G31fx4FfRMqw6WQK1bzOS0W_y-1SQ_wZBep7JwX_e73ABTAHJRJF2d2gQf4VmNG_BeTBGZg1MpICAKY-BEwyseocERk-bufVn8FXOxR-O2cM2QS-A93UmW1fxGDRYyv2F4jlRl8OnSnJ7oXZw4HgX_LSdpLyXqUFFFYTyLQIGguQyWojmgiJoHefKXxECSKtozT-fPOm1RB43ruWU-nCf2m6tBZOdnxiypN96t7g6ZQ6EFaUhALqHiEyLK0rqZi8tuEFDRXwVNHPOG_FaS5956j4wvRlGyPpGUipyXDtf9c6izI4kacTmYjd6ZZboxLxEKiKOwUowMZPZE_u3bhH3s7iKwe4ljEZTfkmVUmKKCmLZxeP9mWmlEkjsrp3ncUmcymBZai8E89Y4v0seSqRCqSUK_yL-tqc_32wwJqgBG1Qyl0CB7djDBrXeJTFD8RcgK3ZW7-k3nM3wBL9FnwdvEAOdZBNgawSDk9KH6wZuMPw4ay-_hg8eZLo5Fi0MxGxC3IpZeb_9-Y4VP_i8SzUHySabuzomzgyraYRCelgX-YGx4B60wjRHgw5ecl0RScgfduxLD2MllZ9QE3Cwm8n-Vxaii9KW9jnZb7-Bc46wp9evxndIXzsN09eTijGvcr4qg536Ibj528cGnT7JZxh3aX6t23AQWhg-NH5Vl5owGMHn47vjwNv59HQ5X9hjQJM6hJCpzp4ogMdE9l8GtgBa8KadyZ83rDLT_6keKnPv1XXmG2zp9oSkJdrjbW2c_3cSSWbueDHuEbZYndiNwPcCQI7JAubhw4-v15flhQg1L_aPbeheCJpI54aGyPm_8hKYKa_-w0E2gT23pHZgKRL1TjS8_u4JKtE2npuy9hFNr4OaOTMCOc_Ef-4TtM7SIlQJ3XBG-o3yFtpQUlGNuuPBTKy-u5T2S3q0g6169mWbmQbf6Fl9PDv8CGcNOFcttMGsurNNvIpUA0TqsWiL-EXWFwaJjmih-dOQRN_ibaZ9a1_aEz7jmSHP7TMHxjvVX3VAEHu0RMebIrUQIuACMxbT_SC2_qb8Ov5wj6Hw7tmjUqTZT9543JKkBo=","summary":[{"type":"summary_text","text":"**Analyzing terminal escape sequence handling**"},{"type":"summary_text","text":"**Clarifying reconnect behavior and error handling**"}]},{"id":"fc_0ce5441d29b9f05d016a5b424d1c50819bb4166adf88f693d3","type":"function_call","status":"completed","arguments":"{\"type\":\"result\",\"result\":{\"data\":{\"summary\":\"Root cause is twofold, with one additional deterministic state-desynchronization seam. (1) The broker/rc transport does not filter spaces: live PTY chunks are forwarded byte-for-byte. Cold attach intentionally omits trailing default spaces and wholly blank rows in `ScreenGrid::render_repaint`, but only after emitting `CSI 2 J`, so that omission is semantically correct on a VT-capable terminal. (2) Final PTY output is not ordered before `Exit`: the drain queues output through the controller/viewer writer, while the independent exit waiter writes `KIND_EXIT` directly to the same physical connection. `Exit` can overtake queued final output, and both `serve_attach` and the rc pump terminate immediately on it. Thus a final `EL`, spaces, `?25h`, `?1049l`, or other teardown sequence can be stranded. The repository already acknowledges this race in `crates/spt-daemon/tests/broker.rs:376-403`. (3) rc has no unconditional terminal-mode teardown. With the status row disabled, every final path—detach, child exit, displacement, 30-second reconnect give-up, first-event stall, and fatal error—drops raw/input modes and prints prose, but emits no SGR reset, cursor-show, scroll-region reset, or alternate-screen leave. A child that exits or disconnects without completing its own teardown therefore leaves the operator terminal dirty. Separately, a cold repaint tracks but does not replay DECSTBM scroll margins, so subsequent live raw output can be interpreted against a different scroll region than the server grid. Picker rendering does not share the PTY/grid path: it uses ratatui’s local previous-buffer diff. Its stale-glyph cause is distinct—`cmd_endpoint_purge` writes to stderr while ratatui still owns the alternate screen, invalidating ratatui’s previous-buffer model; the next diff legitimately omits unchanged blank cells. The only commonality is mutation of the same physical terminal outside the renderer’s state model.\",\"files\":[{\"path\":\"crates/spt-term/src/reader.rs\",\"description\":\"`Drain::spawn` at lines 118-161 reads up to 8192 bytes, scans DSR without removing it, calls `sink(chunk)`, and counts exactly `n`; there is no whitespace/ANSI filtering. This is the PTY-byte origin of the broker path.\"},{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"`OutputLog::append` at lines 903-968 clones the exact chunk into the ring and `output_envelope`, fans it to controller/viewers, then advances `ScreenGrid`. `repaint_initial` at 821-825 substitutes one synthesized repaint only for cold `from_seq == 0`. `become_controller` at 1016-1064 creates the initial batch/writer. `controller_writer` at 1675-1778 serializes queued output. The exit waiter at 3757-3792 independently calls `all_sinks()` and directly `sink.write(&KIND_EXIT)` before removing the session; that direct write is not FIFO-ordered behind the controller/viewer output queues and does not wait for drain EOF. `BrokerConn` cleanup in `handle_conn` at 3465-3500 detaches session subscriptions and FINs/terminal-retires ConnectionBound rc streams.\"},{\"path\":\"crates/spt-daemon/tests/broker.rs\",\"description\":\"Lines 376-403 explicitly document and work around the production ordering fact: `EXIT notification ... can arrive BEFORE the OUTPUT frame`; the test continues draining after Exit. The rc production pump does not. This is direct repository evidence for the final-tail loss race and a natural regression-test location.\"},{\"path\":\"crates/spt-term/src/screen.rs\",\"description\":\"`Cell::is_default_blank` at 141-146 marks only default-pen spaces trimmable. `erase_display`/`erase_line`/`erase_chars` at 347-416 retain erased cells in the model. `render_repaint` at 577-623 emits matching main/alt selection, optional title, `CSI 0 m`, `CSI 2 J`, home, then uses `rposition` and `continue` at 597-599 to omit trailing default blanks and blank rows. Because ED2 precedes it, those omissions are correct for cold repaint; non-default-background blank cells are emitted. However `scroll_top`/`scroll_bottom` are tracked and DECSTBM is parsed at 790-802, but `render_repaint` never emits `CSI <top>;<bottom> r`, leaving client and grid scroll behavior divergent after attach.\"},{\"path\":\"crates/spt-daemon/src/msg.rs\",\"description\":\"`OutputEvent` at 548-558 stores arbitrary PTY bytes as base64. `encode_bytes`, `decode_bytes`, and `output_envelope` at 1164-1184 are byte-preserving; no trim, UTF-8 conversion, or whitespace classification occurs.\"},{\"path\":\"crates/spt-daemon/src/brain.rs\",\"description\":\"`Brain::read_event_until` at 686-755 decodes `OutputEvent` bytes unchanged and applies only sequence dedup/gap rules. `attach`/`attach_as` at 958-1008 arm the cold-repaint pseudo-sequence baseline. No character-level filter exists here.\"},{\"path\":\"crates/spt-daemon/src/attach.rs\",\"description\":\"`serve_attach` forwards `BrokerEvent::Output` unchanged via base64 at 600-609, but on broker `Exit` at 610-617 immediately sends `AttachRecord::Exit` with FIN and returns. It cannot recover a final output frame that arrives after broker Exit. Net-stream EOF cleanup at 568-593 explicitly releases the generation-owned broker role.\"},{\"path\":\"crates/spt-net/src/net/attach.rs\",\"description\":\"`AttachRecord::Output`/`Exit` at lines 103-120 are independent ordered NDJSON records over the stream; output bytes remain base64. The protocol has no `exit_after_seq`/drain watermark that would let the operator defer Exit until the output cursor reaches the final sequence.\"},{\"path\":\"crates/spt/src/rc.rs\",\"description\":\"`pump` at 1837-2020 decodes each Output and calls `stdout.write_all(&bytes)` verbatim at 1972-1993, but returns immediately on `AttachRecord::Exit` at 1995. `run_attach_inner` cleanup at 1254-1268 restores only the optional status row (currently disabled) and drops `RawGuard`; `RawGuard::drop` at 841-856 restores crossterm input mode/mouse/Windows console mode, not child-controlled VT state. Final branches then print prose. `reconnect_banner_bytes` at 1719-1744 does ED2+home, while the reconnect window is 30 seconds at 1659-1663; successful reconnect gets a fresh repaint, but give-up has no terminal restoration. `FIRST_EVENT_GRACE` at 1792-1805 is only a never-received-any-event backstop, not an idle/disconnect timer after the viewport becomes live.\"},{\"path\":\"crates/spt-daemon/src/conn.rs\",\"description\":\"`BrokerConn::write_gated` at 493-610 applies the broker’s absolute 15-second write bound, poisons the entire physical framed connection on timeout/partial-write risk, aborts/severs it, and causes `handle_conn` EOF cleanup. This correctly avoids reusing a potentially partial length-prefixed frame, but it cannot restore local terminal VT modes; that responsibility remains in rc.\"},{\"path\":\"crates/spt/src/picker/mod.rs\",\"description\":\"`setup_terminal`/`restore_terminal` at 95-112 use `ratatui::Terminal<CrosstermBackend<Stdout>>` directly and are unrelated to `spt-term::ScreenGrid`. `event_loop` at 165-178 invokes purge inline while ratatui still owns raw/alternate-screen state, then continues drawing with ratatui’s stale previous Buffer.\"},{\"path\":\"crates/spt/src/cli.rs\",\"description\":\"`cmd_endpoint_purge` at 12707-12826 emits multiple `eprintln!` paths, including `PURGED:{id}` at 12825. When called inline by picker, this out-of-band write mutates the same alternate-screen cursor/display without updating ratatui’s diff baseline, producing stale characters where the next diff skips unchanged spaces.\"},{\"path\":\"docs/adr/0031-server-side-screen-grid-render-repaint.md\",\"description\":\"Governing contract: the grid is the OS-neutral render-replay layer; cold attach gets one synthesized current-screen repaint while live frames remain raw. It confirms picker/PTY content semantics should not be conflated.\"},{\"path\":\"docs/KNOWN-HAZARDS.md\",\"description\":\"Hazards 7.27 and the bounded-write sections describe broker exit/control cleanup and 15-second poisoned-connection retirement. They cover control stamps and transport wedges, but not ordering final output before Exit or restoring the operator’s VT state.\"}],\"architecture\":\"## End-to-end byte path\\n\\n```text\\nPTY child\\n  -> portable_pty master\\n  -> spt_term::Drain::spawn(reader.read)\\n  -> broker OutputLog::append\\n       -> raw ring VecDeque<(seq, Vec<u8>)>\\n       -> controller/viewer writer queue\\n       -> ScreenGrid::advance (state projection, only used for cold attach)\\n  -> OutputEvent { seq, data_b64 }\\n  -> Brain::read_event_until\\n  -> serve_attach: AttachRecord::Output { seq, data_b64 } NDJSON\\n  -> broker-owned loopback/QUIC net stream\\n  -> rc AttachDecoder\\n  -> decode_bytes\\n  -> stdout.write_all(raw bytes)\\n```\\n\\nThere is no character diff or whitespace filter on the live path. The only content transformation is the cold-attach branch:\\n\\n```text\\nfrom_seq == 0 && next_seq > 0\\n  -> ScreenGrid::render_repaint()\\n  -> one Output frame at pseudo-seq next_seq - 1\\n```\\n\\n## Byte-level reproductions\\n\\n### 1. Spaces and erase sequences survive the live path\\n\\nHave the PTY emit:\\n\\n```text\\nhex: 41 20 20 1b 5b 32 4b 0d\\ntext: A␠␠ ESC [ 2 K CR\\n```\\n\\n`Drain::spawn` passes the exact 8-byte slice; `OutputLog::append` stores `bytes.to_vec()`; each envelope base64-encodes and decodes the same bytes; rc writes exactly those 8 bytes. A capture at rc stdout must equal the input. Any mismatch here falsifies the current code reading.\\n\\nOn a cold attach, the original `CSI 2 K` and the two trailing spaces are consumed into grid state rather than replayed literally. The synthesized frame begins with one of `ESC[?1049h` / `ESC[?1049l`, then `ESC[0m ESC[2J ESC[H`. Default blank rows and trailing default spaces are omitted after that full clear. Therefore a terminal initialized with `XXXXXXXXXXXXXXXX` must become blank except for modeled nonblank/non-default-background cells. If it does not, the failure is VT-output interpretation, not missing whitespace in the renderer.\\n\\n### 2. Final output can be overtaken by Exit\\n\\nUse a short-lived PTY child whose final single write is:\\n\\n```text\\nhex: 58 58 58 58 1b 5b 32 4b 1b 5b 3f 32 35 68 1b 5b 3f 31 30 34 39 6c\\nmeaning: \\\"XXXX\\\" + CSI 2 K + show-cursor + leave-alt-screen\\n```\\n\\nThen exit immediately. The drain thread can enqueue this as `CtrlMsg::Output`, while the waiter thread, after `wait()`, calls `sink.write(KIND_EXIT)` directly. If the direct write acquires the connection gate first, the observable frame order is `Exit` then `Output`. `serve_attach` turns Exit into a FINished attach record and returns; rc returns on that record, so the final output bytes are never rendered. Capture broker IPC frame kinds to falsify/confirm. The existing env-injection integration test already observes this ordering and compensates by continuing to drain after Exit.\\n\\n### 3. rc omits final VT restoration even without the broker race\\n\\nFeed rc a final rendered chunk containing:\\n\\n```text\\nESC[?1049h ESC[?25l ESC[31m DIRTY\\n```\\n\\nThen produce any final outcome—operator `Ctrl-B d`, `AttachRecord::Exit`, displacement, first-event stall, reconnect give-up, or fatal read/write error. With `STATUS_ROW_ENABLED == false`, capture bytes written after the pump ends. The current output is only CRLF plus the bracketed status prose. It contains none of:\\n\\n```text\\nESC[0m       reset SGR\\nESC[?25h    show cursor\\nESC[r       reset scroll margins\\nESC[?1049l  leave alternate screen\\n```\\n\\nThat is deterministic and independent of OS transport.\\n\\n### 4. Cold repaint loses the child’s scroll-region mode\\n\\nFeed `ScreenGrid`:\\n\\n```text\\nESC[?1049h ESC[2;23r ESC[23;1H bottom\\n```\\n\\nThe grid records `scroll_top=1`, `scroll_bottom=22`, but `render_repaint()` contains no `ESC[2;23r`. Apply the repaint to a clean terminal, then feed a raw LF/IND at the bottom margin. The server model scrolls rows 2–23 while the client uses its default full-screen margin, producing divergent cell placement/stale rows. A unit assertion that repaint contains the tracked DECSTBM sequence is the deterministic seam.\\n\\n### 5. Picker’s distinct diff desynchronization\\n\\nWhile picker is on `ConfirmPurge`, `cmd_endpoint_purge(id, true, false)` writes `PURGED:<id>\\\\n` to stderr inside the active alternate screen. Ratatui’s previous `Buffer` is unchanged. The model removes the row and draws again; ratatui emits only cells differing from its previous buffer, so unchanged spaces are not rewritten over the externally written glyphs. A stateful recording/TestBackend that allows an external display mutation between two `Terminal::draw` calls reproduces this exactly. It shares neither `ScreenGrid` nor rc’s output pump.\\n\\n## Shared versus distinct causes\\n\\n- **No common renderer/diff/filter:** picker = ratatui local buffer diff; broker cold attach = `spt-term::ScreenGrid` full synthesized repaint; rc live attach = raw `write_all`.\\n- **Picker cause:** out-of-band stderr write invalidates ratatui’s assumed previous screen, so its correct diff omits unchanged blank cells.\\n- **Broker/rc cause:** output-vs-Exit is cross-thread/out-of-queue ordering, followed by immediate termination; final VT state restoration is absent client-side. Cold repaint also omits tracked DECSTBM state.\\n- **Common abstract failure only:** both allow the physical terminal to be mutated or terminated outside the state owner’s ordered render lifecycle. This is architectural similarity, not shared code.\\n\\n## Fix suggestions, ordered by source correctness\\n\\n1. **Make session drain the terminal-event sequencer.** Do not have the exit waiter direct-write Exit through `all_sinks`. Wait until the PTY drain has observed EOF/completion, then enqueue Exit behind every prior Output on each controller/viewer writer queue. The drain/output queue must be FIFO authority for both Output and terminal Exit. A mere mutex is insufficient: the existing physical-connection gate serializes individual writes but does not impose producer order.\\n2. **Add an explicit final-output watermark if protocol-level defense is desired.** Extend Exit additively with `after_seq`/final sequence. `serve_attach` and rc should not terminate until the render cursor has reached it. This guards future cross-thread reorder, but it is secondary to fixing broker source ordering.\\n3. **Unconditionally restore the operator terminal in rc via RAII.** Separate OS raw/console-mode restoration from VT display restoration. Before printing any final prose, emit/reset through a display guard: SGR reset, full scroll-region reset, cursor show, and leave alternate screen (prefer crossterm commands where exact semantics are portable). Run on every return/error/panic-safe drop path, not only when the disabled `StatusRow` exists. Preserve the current Windows output-mode restoration after the cleanup bytes have been emitted while VT processing is still enabled.\\n4. **Replay all render-affecting tracked modes in `ScreenGrid::render_repaint`.** At minimum emit tracked DECSTBM before the final cursor placement. If the grid later tracks DECOM/autowrap/tab stops, they must be emitted too. Add a stateful emulator contract test: dirty initial screen + repaint + one subsequent raw frame must match the server grid, not merely substring-test repaint text.\\n5. **Fix picker independently.** Never run a stderr-writing command while ratatui owns the alternate screen. Either make purge return structured diagnostics without writing, temporarily restore/suspend the terminal before calling it and re-enter with a fresh `Terminal`, or force a backend clear/reset of ratatui’s previous buffer before the next draw. The strongest test mutates backend display state out of band between draws and asserts the next frame reconstructs the complete model.\\n6. **Regression coverage:** add (a) broker integration asserting final whitespace/EL/leave-alt Output precedes Exit, (b) rc sink capture asserting teardown bytes on every `PumpEnd` and error class, (c) `ScreenGrid` scroll-margin round-trip, and (d) picker out-of-band mutation recovery. Existing isolated view snapshots cannot catch a previous-buffer desync.\"}}}","call_id":"call_YDHytc3RzR8YEiK4HQkCVLud","name":"yield"}]},"duration":140825.2132999897,"ttft":2916.4979000091553,"contextSnapshot":{"promptTokens":211470,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_YDHytc3RzR8YEiK4HQkCVLud|fc_0ce5441d29b9f05d016a5b424d1c50819bb4166adf88f693d3","toolName":"yield","startedAt":"2026-07-18T09:08:42.513Z"},"id":"06acd56c","parentId":"e38284bf","timestamp":"2026-07-18T09:08:42.513Z"}
{"type":"message","id":"3eaa0d46","parentId":"06acd56c","timestamp":"2026-07-18T09:08:42.514Z","message":{"role":"toolResult","toolCallId":"call_YDHytc3RzR8YEiK4HQkCVLud|fc_0ce5441d29b9f05d016a5b424d1c50819bb4166adf88f693d3","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Root cause is twofold, with one additional deterministic state-desynchronization seam. (1) The broker/rc transport does not filter spaces: live PTY chunks are forwarded byte-for-byte. Cold attach intentionally omits trailing default spaces and wholly blank rows in `ScreenGrid::render_repaint`, but only after emitting `CSI 2 J`, so that omission is semantically correct on a VT-capable terminal. (2) Final PTY output is not ordered before `Exit`: the drain queues output through the controller/viewer writer, while the independent exit waiter writes `KIND_EXIT` directly to the same physical connection. `Exit` can overtake queued final output, and both `serve_attach` and the rc pump terminate immediately on it. Thus a final `EL`, spaces, `?25h`, `?1049l`, or other teardown sequence can be stranded. The repository already acknowledges this race in `crates/spt-daemon/tests/broker.rs:376-403`. (3) rc has no unconditional terminal-mode teardown. With the status row disabled, every final path—detach, child exit, displacement, 30-second reconnect give-up, first-event stall, and fatal error—drops raw/input modes and prints prose, but emits no SGR reset, cursor-show, scroll-region reset, or alternate-screen leave. A child that exits or disconnects without completing its own teardown therefore leaves the operator terminal dirty. Separately, a cold repaint tracks but does not replay DECSTBM scroll margins, so subsequent live raw output can be interpreted against a different scroll region than the server grid. Picker rendering does not share the PTY/grid path: it uses ratatui’s local previous-buffer diff. Its stale-glyph cause is distinct—`cmd_endpoint_purge` writes to stderr while ratatui still owns the alternate screen, invalidating ratatui’s previous-buffer model; the next diff legitimately omits unchanged blank cells. The only commonality is mutation of the same physical terminal outside the renderer’s state model.","files":[{"path":"crates/spt-term/src/reader.rs","description":"`Drain::spawn` at lines 118-161 reads up to 8192 bytes, scans DSR without removing it, calls `sink(chunk)`, and counts exactly `n`; there is no whitespace/ANSI filtering. This is the PTY-byte origin of the broker path."},{"path":"crates/spt-daemon/src/broker.rs","description":"`OutputLog::append` at lines 903-968 clones the exact chunk into the ring and `output_envelope`, fans it to controller/viewers, then advances `ScreenGrid`. `repaint_initial` at 821-825 substitutes one synthesized repaint only for cold `from_seq == 0`. `become_controller` at 1016-1064 creates the initial batch/writer. `controller_writer` at 1675-1778 serializes queued output. The exit waiter at 3757-3792 independently calls `all_sinks()` and directly `sink.write(&KIND_EXIT)` before removing the session; that direct write is not FIFO-ordered behind the controller/viewer output queues and does not wait for drain EOF. `BrokerConn` cleanup in `handle_conn` at 3465-3500 detaches session subscriptions and FINs/terminal-retires ConnectionBound rc streams."},{"path":"crates/spt-daemon/tests/broker.rs","description":"Lines 376-403 explicitly document and work around the production ordering fact: `EXIT notification ... can arrive BEFORE the OUTPUT frame`; the test continues draining after Exit. The rc production pump does not. This is direct repository evidence for the final-tail loss race and a natural regression-test location."},{"path":"crates/spt-term/src/screen.rs","description":"`Cell::is_default_blank` at 141-146 marks only default-pen spaces trimmable. `erase_display`/`erase_line`/`erase_chars` at 347-416 retain erased cells in the model. `render_repaint` at 577-623 emits matching main/alt selection, optional title, `CSI 0 m`, `CSI 2 J`, home, then uses `rposition` and `continue` at 597-599 to omit trailing default blanks and blank rows. Because ED2 precedes it, those omissions are correct for cold repaint; non-default-background blank cells are emitted. However `scroll_top`/`scroll_bottom` are tracked and DECSTBM is parsed at 790-802, but `render_repaint` never emits `CSI <top>;<bottom> r`, leaving client and grid scroll behavior divergent after attach."},{"path":"crates/spt-daemon/src/msg.rs","description":"`OutputEvent` at 548-558 stores arbitrary PTY bytes as base64. `encode_bytes`, `decode_bytes`, and `output_envelope` at 1164-1184 are byte-preserving; no trim, UTF-8 conversion, or whitespace classification occurs."},{"path":"crates/spt-daemon/src/brain.rs","description":"`Brain::read_event_until` at 686-755 decodes `OutputEvent` bytes unchanged and applies only sequence dedup/gap rules. `attach`/`attach_as` at 958-1008 arm the cold-repaint pseudo-sequence baseline. No character-level filter exists here."},{"path":"crates/spt-daemon/src/attach.rs","description":"`serve_attach` forwards `BrokerEvent::Output` unchanged via base64 at 600-609, but on broker `Exit` at 610-617 immediately sends `AttachRecord::Exit` with FIN and returns. It cannot recover a final output frame that arrives after broker Exit. Net-stream EOF cleanup at 568-593 explicitly releases the generation-owned broker role."},{"path":"crates/spt-net/src/net/attach.rs","description":"`AttachRecord::Output`/`Exit` at lines 103-120 are independent ordered NDJSON records over the stream; output bytes remain base64. The protocol has no `exit_after_seq`/drain watermark that would let the operator defer Exit until the output cursor reaches the final sequence."},{"path":"crates/spt/src/rc.rs","description":"`pump` at 1837-2020 decodes each Output and calls `stdout.write_all(&bytes)` verbatim at 1972-1993, but returns immediately on `AttachRecord::Exit` at 1995. `run_attach_inner` cleanup at 1254-1268 restores only the optional status row (currently disabled) and drops `RawGuard`; `RawGuard::drop` at 841-856 restores crossterm input mode/mouse/Windows console mode, not child-controlled VT state. Final branches then print prose. `reconnect_banner_bytes` at 1719-1744 does ED2+home, while the reconnect window is 30 seconds at 1659-1663; successful reconnect gets a fresh repaint, but give-up has no terminal restoration. `FIRST_EVENT_GRACE` at 1792-1805 is only a never-received-any-event backstop, not an idle/disconnect timer after the viewport becomes live."},{"path":"crates/spt-daemon/src/conn.rs","description":"`BrokerConn::write_gated` at 493-610 applies the broker’s absolute 15-second write bound, poisons the entire physical framed connection on timeout/partial-write risk, aborts/severs it, and causes `handle_conn` EOF cleanup. This correctly avoids reusing a potentially partial length-prefixed frame, but it cannot restore local terminal VT modes; that responsibility remains in rc."},{"path":"crates/spt/src/picker/mod.rs","description":"`setup_terminal`/`restore_terminal` at 95-112 use `ratatui::Terminal<CrosstermBackend<Stdout>>` directly and are unrelated to `spt-term::ScreenGrid`. `event_loop` at 165-178 invokes purge inline while ratatui still owns raw/alternate-screen state, then continues drawing with ratatui’s stale previous Buffer."},{"path":"crates/spt/src/cli.rs","description":"`cmd_endpoint_purge` at 12707-12826 emits multiple `eprintln!` paths, including `PURGED:{id}` at 12825. When called inline by picker, this out-of-band write mutates the same alternate-screen cursor/display without updating ratatui’s diff baseline, producing stale characters where the next diff skips unchanged spaces."},{"path":"docs/adr/0031-server-side-screen-grid-render-repaint.md","description":"Governing contract: the grid is the OS-neutral render-replay layer; cold attach gets one synthesized current-screen repaint while live frames remain raw. It confirms picker/PTY content semantics should not be conflated."},{"path":"docs/KNOWN-HAZARDS.md","description":"Hazards 7.27 and the bounded-write sections describe broker exit/control cleanup and 15-second poisoned-connection retirement. They cover control stamps and transport wedges, but not ordering final output before Exit or restoring the operator’s VT state."}],"architecture":"## End-to-end byte path\n\n```text\nPTY child\n  -> portable_pty master\n  -> spt_term::Drain::spawn(reader.read)\n  -> broker OutputLog::append\n       -> raw ring VecDeque<(seq, Vec<u8>)>\n       -> controller/viewer writer queue\n       -> ScreenGrid::advance (state projection, only used for cold attach)\n  -> OutputEvent { seq, data_b64 }\n  -> Brain::read_event_until\n  -> serve_attach: AttachRecord::Output { seq, data_b64 } NDJSON\n  -> broker-owned loopback/QUIC net stream\n  -> rc AttachDecoder\n  -> decode_bytes\n  -> stdout.write_all(raw bytes)\n```\n\nThere is no character diff or whitespace filter on the live path. The only content transformation is the cold-attach branch:\n\n```text\nfrom_seq == 0 && next_seq > 0\n  -> ScreenGrid::render_repaint()\n  -> one Output frame at pseudo-seq next_seq - 1\n```\n\n## Byte-level reproductions\n\n### 1. Spaces and erase sequences survive the live path\n\nHave the PTY emit:\n\n```text\nhex: 41 20 20 1b 5b 32 4b 0d\ntext: A␠␠ ESC [ 2 K CR\n```\n\n`Drain::spawn` passes the exact 8-byte slice; `OutputLog::append` stores `bytes.to_vec()`; each envelope base64-encodes and decodes the same bytes; rc writes exactly those 8 bytes. A capture at rc stdout must equal the input. Any mismatch here falsifies the current code reading.\n\nOn a cold attach, the original `CSI 2 K` and the two trailing spaces are consumed into grid state rather than replayed literally. The synthesized frame begins with one of `ESC[?1049h` / `ESC[?1049l`, then `ESC[0m ESC[2J ESC[H`. Default blank rows and trailing default spaces are omitted after that full clear. Therefore a terminal initialized with `XXXXXXXXXXXXXXXX` must become blank except for modeled nonblank/non-default-background cells. If it does not, the failure is VT-output interpretation, not missing whitespace in the renderer.\n\n### 2. Final output can be overtaken by Exit\n\nUse a short-lived PTY child whose final single write is:\n\n```text\nhex: 58 58 58 58 1b 5b 32 4b 1b 5b 3f 32 35 68 1b 5b 3f 31 30 34 39 6c\nmeaning: \"XXXX\" + CSI 2 K + show-cursor + leave-alt-screen\n```\n\nThen exit immediately. The drain thread can enqueue this as `CtrlMsg::Output`, while the waiter thread, after `wait()`, calls `sink.write(KIND_EXIT)` directly. If the direct write acquires the connection gate first, the observable frame order is `Exit` then `Output`. `serve_attach` turns Exit into a FINished attach record and returns; rc returns on that record, so the final output bytes are never rendered. Capture broker IPC frame kinds to falsify/confirm. The existing env-injection integration test already observes this ordering and compensates by continuing to drain after Exit.\n\n### 3. rc omits final VT restoration even without the broker race\n\nFeed rc a final rendered chunk containing:\n\n```text\nESC[?1049h ESC[?25l ESC[31m DIRTY\n```\n\nThen produce any final outcome—operator `Ctrl-B d`, `AttachRecord::Exit`, displacement, first-event stall, reconnect give-up, or fatal read/write error. With `STATUS_ROW_ENABLED == false`, capture bytes written after the pump ends. The current output is only CRLF plus the bracketed status prose. It contains none of:\n\n```text\nESC[0m       reset SGR\nESC[?25h    show cursor\nESC[r       reset scroll margins\nESC[?1049l  leave alternate screen\n```\n\nThat is deterministic and independent of OS transport.\n\n### 4. Cold repaint loses the child’s scroll-region mode\n\nFeed `ScreenGrid`:\n\n```text\nESC[?1049h ESC[2;23r ESC[23;1H bottom\n```\n\nThe grid records `scroll_top=1`, `scroll_bottom=22`, but `render_repaint()` contains no `ESC[2;23r`. Apply the repaint to a clean terminal, then feed a raw LF/IND at the bottom margin. The server model scrolls rows 2–23 while the client uses its default full-screen margin, producing divergent cell placement/stale rows. A unit assertion that repaint contains the tracked DECSTBM sequence is the deterministic seam.\n\n### 5. Picker’s distinct diff desynchronization\n\nWhile picker is on `ConfirmPurge`, `cmd_endpoint_purge(id, true, false)` writes `PURGED:<id>\\n` to stderr inside the active alternate screen. Ratatui’s previous `Buffer` is unchanged. The model removes the row and draws again; ratatui emits only cells differing from its previous buffer, so unchanged spaces are not rewritten over the externally written glyphs. A stateful recording/TestBackend that allows an external display mutation between two `Terminal::draw` calls reproduces this exactly. It shares neither `ScreenGrid` nor rc’s output pump.\n\n## Shared versus distinct causes\n\n- **No common renderer/diff/filter:** picker = ratatui local buffer diff; broker cold attach = `spt-term::ScreenGrid` full synthesized repaint; rc live attach = raw `write_all`.\n- **Picker cause:** out-of-band stderr write invalidates ratatui’s assumed previous screen, so its correct diff omits unchanged blank cells.\n- **Broker/rc cause:** output-vs-Exit is cross-thread/out-of-queue ordering, followed by immediate termination; final VT state restoration is absent client-side. Cold repaint also omits tracked DECSTBM state.\n- **Common abstract failure only:** both allow the physical terminal to be mutated or terminated outside the state owner’s ordered render lifecycle. This is architectural similarity, not shared code.\n\n## Fix suggestions, ordered by source correctness\n\n1. **Make session drain the terminal-event sequencer.** Do not have the exit waiter direct-write Exit through `all_sinks`. Wait until the PTY drain has observed EOF/completion, then enqueue Exit behind every prior Output on each controller/viewer writer queue. The drain/output queue must be FIFO authority for both Output and terminal Exit. A mere mutex is insufficient: the existing physical-connection gate serializes individual writes but does not impose producer order.\n2. **Add an explicit final-output watermark if protocol-level defense is desired.** Extend Exit additively with `after_seq`/final sequence. `serve_attach` and rc should not terminate until the render cursor has reached it. This guards future cross-thread reorder, but it is secondary to fixing broker source ordering.\n3. **Unconditionally restore the operator terminal in rc via RAII.** Separate OS raw/console-mode restoration from VT display restoration. Before printing any final prose, emit/reset through a display guard: SGR reset, full scroll-region reset, cursor show, and leave alternate screen (prefer crossterm commands where exact semantics are portable). Run on every return/error/panic-safe drop path, not only when the disabled `StatusRow` exists. Preserve the current Windows output-mode restoration after the cleanup bytes have been emitted while VT processing is still enabled.\n4. **Replay all render-affecting tracked modes in `ScreenGrid::render_repaint`.** At minimum emit tracked DECSTBM before the final cursor placement. If the grid later tracks DECOM/autowrap/tab stops, they must be emitted too. Add a stateful emulator contract test: dirty initial screen + repaint + one subsequent raw frame must match the server grid, not merely substring-test repaint text.\n5. **Fix picker independently.** Never run a stderr-writing command while ratatui owns the alternate screen. Either make purge return structured diagnostics without writing, temporarily restore/suspend the terminal before calling it and re-enter with a fresh `Terminal`, or force a backend clear/reset of ratatui’s previous buffer before the next draw. The strongest test mutates backend display state out of band between draws and asserts the next frame reconstructs the complete model.\n6. **Regression coverage:** add (a) broker integration asserting final whitespace/EL/leave-alt Output precedes Exit, (b) rc sink capture asserting teardown bytes on every `PumpEnd` and error class, (c) `ScreenGrid` scroll-margin round-trip, and (d) picker out-of-band mutation recovery. Existing isolated view snapshots cannot catch a previous-buffer desync."},"status":"success","type":"result"},"isError":false,"timestamp":1784365722513}}
{"type":"message","id":"904500e3","parentId":"3eaa0d46","timestamp":"2026-07-18T09:08:42.535Z","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":1784365722532}}
{"type":"message","id":"a857ab30","parentId":"904500e3","timestamp":"2026-07-18T09:08:42.538Z","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\nPlease return current source-grounded findings now; prioritize exact cause/fix over exhaustive context.","attribution":"agent","timestamp":1784365683741,"steering":true}}
{"type":"message","id":"580ccb29","parentId":"a857ab30","timestamp":"2026-07-18T09:09:09.617Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Sending concise findings via IRC**"},{"type":"toolCall","id":"call_qHq44HTn7jisq8on0sJGtIj4|fc_0d9d15bf8b62bf27016a5b42b590448199b6fda5c729d17152","name":"irc","arguments":{"i":"Returning PTY root cause","op":"send","to":"Main","message":"Source-grounded RCA: live PTY path does NOT trim spaces (`spt-term/reader.rs:118-161` -> `broker.rs OutputLog::append:903-968` -> base64 -> `attach.rs:600-609` -> `rc.rs:1966-1993 write_all`). Cold `ScreenGrid::render_repaint` (`screen.rs:577-623`) omits trailing default blanks/blank rows only after `ESC[2J`, semantically safe. Real loss: broker exit waiter `broker.rs:3757-3792` direct-writes KIND_EXIT via `all_sinks`, racing/overtaking output queued through `controller_writer:1675-1778`; serve (`attach.rs:610-617`) and rc (`rc.rs:1995`) return immediately on Exit, dropping final EL/spaces/?25h/?1049l. Existing test explicitly admits EXIT-before-OUTPUT at `tests/broker.rs:376-403`. Also rc final cleanup `rc.rs:1254-1268` only restores disabled StatusRow + RawGuard; no unconditional SGR reset/cursor show/scroll reset/leave-alt on detach/exit/displace/30s reconnect give-up/error. Grid tracks DECSTBM (`screen.rs:790-802`) but repaint never emits it, so later raw scrolling diverges. Fix: make drain/output writer FIFO sole sequencer; enqueue Exit only after drain EOF + prior output, optionally Exit.after_seq; add rc VT RAII restore before restoring Windows output mode; replay DECSTBM. Picker is distinct ratatui diff desync: inline purge `picker/mod.rs:165-178` calls stderr-writing `cli.rs:12707-12826` while alt-screen active, invalidating prior Buffer so next diff skips blanks.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":7403,"output":480,"cacheRead":210432,"cacheWrite":0,"totalTokens":218315,"reasoningTokens":56,"cost":{"input":0.037015000000000006,"output":0.0144,"cacheRead":0.10521599999999999,"cacheWrite":0,"total":0.156631}},"stopReason":"toolUse","timestamp":1784365722548,"responseId":"resp_0d9d15bf8b62bf27016a5b429af3908199b7685275cf6a5885","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0d9d15bf8b62bf27016a5b429f61488199802c8dcbbb6e6ee2","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0K1TMl30LEdkdguCP5ahu_HJk5aQV0tIUVESwcqIs2b1HySN4bY-Tb260Q10YT3G4T-wrfZiPwOt4Zovumwo4iz8PHwY6jKpHuiI8_Xt3PrKC80FNzBz1njJdQmVlfkLD-o82kGofFWuyennRxwAnQZn0RaJjHaebmknD2k8XYz4Kj4csL1_FeXhVyXzZ50KDBD-n8ZVgnYWGbGHg7SnDvoFoRb7MAh7gX7FpFF2fR7qFyT9cMVGoD2M8wlooemu2WOnylbLaE8_zQ1rhFRxanjLKcWLLIcpKCVQfBPa-hoRFasMNjP6r6rbGXQfooUHsUP1Kpn0sI97ka_rz94uyXK8saaDM3aGDYc4CiV3Yc1eiU99EH_5f9HXIsEhWH957rUyFcRXyB-K3G_C8TwctPr-kI38rdPUWMyXn5sRJBM_ooHAMyCnpsOctonQ6Csgr21YOMHQ_fdk3AaEaBWBFsCs7zC8G1cSBpQeKxIMZz6qQYqq8tDAfYoSrpNnh_FEcLxfE9WFOQrIwxhsUBR3iVWCYbMvOrL6HS1uvn3IDFGA-grdj8P45KBWsTw5C9AY_Rpzb7DJwnQpZXqthVTa9UqfkPqbLG6cBjCdqxpJ43auw0459KxoplGqFFhWlRoTUut9pjuf4BMIwrHS51h6jcKEc-JZHrWqlClokBA3Tlj2Tiyq6QogJao49SZNpwdnxh-2unUhgCqtHSrHhmUrsYPuZ3DBnT_y2srPsO9qS6vIjcdIncpt_SyexikWmPsyJQH-NnAKk-nMlQvtPV6SeXfeQzuzu8lnvE8GF_iXj_wZTaQW29z9lqROF2IZ4eC4a5DNeEEZDx5WA6_JrmXfkHtNdPXH559_q1odET6qx-SbuhPx82MaD_PwM1fmNWVlVeEuetIDs6Rul20aXV8fpxHe2b-nHMi2T_tgdafi-z7_R9U5ukr_4oqV7tOK0JRfaD8YEdUs9Itq6mLrD-cidbGb-dJf3RfaENbQy3AKaRcuYCSSH-TxJ9DxaOEqBpO72I03uhA_sDL4GINSv6PVsOzVQ-kZlMTwoqk3rqsolkdIESa6IgAA_KyxKKEbi22tvA5_q6VvA0_Uupl6hQmfabpOI7OwYlKasF8HjdGlEq4FNsVcBPZQVpCJJKfMhh9IO58UmryjadJJOFxvFL5UafwdX354t3heSx1TRBGJ3VtV9IAgIK_EsJ4_PWWnJAUZN6AAtxn9r6Bqpk_bp4dRkTFlPpV2M-4icTmc5mLBk0nL2RnC3w1jHjDVhZZl7_fUHyT","summary":[{"type":"summary_text","text":"**Sending concise findings via IRC**"}]},{"id":"fc_0d9d15bf8b62bf27016a5b42b590448199b6fda5c729d17152","type":"function_call","status":"completed","arguments":"{\"i\":\"Returning PTY root cause\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Source-grounded RCA: live PTY path does NOT trim spaces (`spt-term/reader.rs:118-161` -> `broker.rs OutputLog::append:903-968` -> base64 -> `attach.rs:600-609` -> `rc.rs:1966-1993 write_all`). Cold `ScreenGrid::render_repaint` (`screen.rs:577-623`) omits trailing default blanks/blank rows only after `ESC[2J`, semantically safe. Real loss: broker exit waiter `broker.rs:3757-3792` direct-writes KIND_EXIT via `all_sinks`, racing/overtaking output queued through `controller_writer:1675-1778`; serve (`attach.rs:610-617`) and rc (`rc.rs:1995`) return immediately on Exit, dropping final EL/spaces/?25h/?1049l. Existing test explicitly admits EXIT-before-OUTPUT at `tests/broker.rs:376-403`. Also rc final cleanup `rc.rs:1254-1268` only restores disabled StatusRow + RawGuard; no unconditional SGR reset/cursor show/scroll reset/leave-alt on detach/exit/displace/30s reconnect give-up/error. Grid tracks DECSTBM (`screen.rs:790-802`) but repaint never emits it, so later raw scrolling diverges. Fix: make drain/output writer FIFO sole sequencer; enqueue Exit only after drain EOF + prior output, optionally Exit.after_seq; add rc VT RAII restore before restoring Windows output mode; replay DECSTBM. Picker is distinct ratatui diff desync: inline purge `picker/mod.rs:165-178` calls stderr-writing `cli.rs:12707-12826` while alt-screen active, invalidating prior Buffer so next diff skips blanks.\",\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_qHq44HTn7jisq8on0sJGtIj4","name":"irc"}]},"duration":27068.68250000477,"ttft":4833.422699987888,"contextSnapshot":{"promptTokens":217835,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qHq44HTn7jisq8on0sJGtIj4|fc_0d9d15bf8b62bf27016a5b42b590448199b6fda5c729d17152","toolName":"irc","startedAt":"2026-07-18T09:09:09.618Z","intent":"Returning PTY root cause"},"id":"9ec74abe","parentId":"580ccb29","timestamp":"2026-07-18T09:09:09.618Z"}
{"type":"message","id":"d23e6179","parentId":"9ec74abe","timestamp":"2026-07-18T09:09:09.618Z","message":{"role":"toolResult","toolCallId":"call_qHq44HTn7jisq8on0sJGtIj4|fc_0d9d15bf8b62bf27016a5b42b590448199b6fda5c729d17152","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"RcWhitespaceRca","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784365749618}}
{"type":"message","id":"29534c90","parentId":"d23e6179","timestamp":"2026-07-18T09:09:33.654Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"toolCall","id":"call_R6vhGUoe7OaP3TapqVLS7TPa|fc_0d9d15bf8b62bf27016a5b42cafad481999290ca7539057938","name":"yield","arguments":{"type":"result","result":{"data":{"summary":"Exact cause: spaces are not filtered on the live PTY/rc path. The deterministic loss is terminal `Exit` overtaking queued final PTY output, followed by rc immediately terminating without unconditional VT restoration. Cold repaint’s omitted blank cells are intentional because it first emits ED2. Picker is a separate ratatui previous-buffer desynchronization, not a shared renderer.","files":[{"path":"crates/spt-daemon/src/broker.rs","description":"`OutputLog::append` lines 903-968 queues exact raw output; `controller_writer` lines 1675-1778 drains it asynchronously. The independent exit waiter lines 3757-3792 direct-writes `KIND_EXIT` through `all_sinks`, so Exit can overtake queued final output."},{"path":"crates/spt-daemon/tests/broker.rs","description":"Lines 376-403 explicitly state that EXIT can arrive before OUTPUT and make the test continue draining after Exit. Production rc does not continue."},{"path":"crates/spt/src/rc.rs","description":"`pump` lines 1966-1995 writes Output bytes verbatim but immediately returns on Exit. Cleanup lines 1254-1268 restores only the optional, currently-disabled status row and drops `RawGuard`; it emits no unconditional SGR reset, cursor show, scroll-region reset, or alternate-screen leave."},{"path":"crates/spt-term/src/screen.rs","description":"`render_repaint` lines 577-623 emits `ESC[0m ESC[2J ESC[H` before trimming trailing default spaces and skipping blank rows, so those omissions are correct. DECSTBM is tracked at lines 790-802 but never replayed, allowing later live scrolling to diverge."},{"path":"crates/spt-term/src/reader.rs","description":"`Drain::spawn` lines 118-161 forwards every byte read from the PTY; no whitespace or ANSI filtering."},{"path":"crates/spt-daemon/src/attach.rs","description":"Lines 600-609 base64-forward Output unchanged; lines 610-617 send Exit+FIN and return immediately, so late final Output cannot recover."},{"path":"crates/spt/src/picker/mod.rs","description":"Lines 165-178 invoke purge inline while ratatui still owns the alternate screen; this path does not use ScreenGrid or rc."},{"path":"crates/spt/src/cli.rs","description":"`cmd_endpoint_purge` lines 12707-12826 writes diagnostics/PURGED to stderr out-of-band, invalidating ratatui’s previous-buffer diff model."}],"architecture":"Byte path is PTY -> `Drain::spawn` -> `OutputLog::append` -> `OutputEvent` base64 -> `Brain::read_event_until` -> `serve_attach` `AttachRecord::Output` -> loopback/QUIC -> rc decode -> `stdout.write_all`; it is byte-preserving. Reproduce the real race with a child that writes `XXXX ESC[2K ESC[?25h ESC[?1049l` and exits immediately: capture broker frame order; current code can emit Exit before that Output, and rc stops at Exit. Fix at source by making the drain/output writer FIFO the sole sequencer: observe drain EOF, then enqueue Exit behind all prior Output for controller and viewers; optionally add an Exit final-sequence watermark. Add rc display-state RAII that, on every exit/error/reconnect-give-up path and before Windows VT-output mode is restored, emits SGR reset, scroll-margin reset, cursor show, and leave-alt-screen. Replay tracked DECSTBM in cold repaint. Fix picker separately by preventing stderr writes while ratatui owns the screen or clearing/resetting ratatui’s previous buffer before the next draw."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":737,"output":833,"cacheRead":217600,"cacheWrite":0,"totalTokens":219170,"reasoningTokens":24,"cost":{"input":0.0036850000000000003,"output":0.024990000000000002,"cacheRead":0.1088,"cacheWrite":0,"total":0.13747499999999999}},"stopReason":"toolUse","timestamp":1784365749630,"responseId":"resp_0d9d15bf8b62bf27016a5b42b5e13c8199830cc61fdcc47b31","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_0d9d15bf8b62bf27016a5b42b7502c819994f612627a571818","type":"reasoning","content":[],"encrypted_content":"gAAAAABqW0LKxirVchwp6EcQMeM3wSorKr_7Ins0tf7bbBqMF2CPAdvDUdHwY42zAV3xUR-JzhM-8fcSBeW-jYo7l7u5-1x2n1feSGj6ZqC6e3RpNrmJ5IaBKJ25k5mDUSpovxLpIT36YHtLO8eD9wQaT3iyDVge4aEfaLWHORtpmD0V_c1Ov9H-17x3rT_WAGRYMNeCkQDU_VMPagMMLUVP1aoPiTtuOdGJuQKOXELp6ffkKnpJcJtJ781yPOPsiuXl2IoHIj8XelFsYouDdxNeWT2pH5zYEiJFEBRDLWslYfK8MEKGyazO-Ub5qdzWN9eUj7kHthmzQjMUTKauKpHDSFf12muTcqAKHGvYWw0EDZWRyrij4XbfhIV7rwtqKkB55RfXtN5e0TEg01n1xQPl1a-qJeV8yeGcE0_c50R4xnXwsrDHZlHKegNH8_bwv90XCnyM-5fsN18NNNzsWeHryhJbMUXxjhRWnPLGffEEBFMMmAArajinM3zYb8eEDSzj3xlQavDMxL-_SIwuH8jkUtxaucJVucezLEfIWeyX81tmW8wT_Xbl1sXgFMUJhwQR3Vy_y_xAvcAnR3ytk3g1HaGxWV20K0ojTpHyqjyFu1gNfrgUk-aCIIEQwQbHnOrlhwFewVn5KVpwkTv2iiV70ymy4EF5WY_99yqbTa8tHqYqJ4MzdxoTxT5qe5ooVXRWU8Yihxmc8VXHMK5uTRsBWzUCKv-gVJKuvvc3fzQk4-RKN915rFOr6XVUNokIZYZFRBKKXscQJitCNk5CSs5wKki33SQzD086HQf4wqajo6N0Oksg4naB5HsSKE36tIVBPjLjBZRkqAGW8INDRM4SD5JDwFG1nXePzTFovDmAcQootnX7sT5D4u4aDRy-uP1edXgwxMlOzpyM5sgkdTJM6NmqvBxW9GcaV7B2Jn_1mzPUO-QNxl9B2XEJutGXVmKM5m5VtqrXnw4UaO5jIyRPaXy_GEywaDwvJkr9twBYb3ITxSVK8bv3OLbvNRc5JI0s1P4xVjen3_5F40pZNf1yje1yYCZ6QirKAfYlGYj4srTD4-ZWXmnGjMt52n0dMrzQIoMCaeYE","summary":[]},{"id":"fc_0d9d15bf8b62bf27016a5b42cafad481999290ca7539057938","type":"function_call","status":"completed","arguments":"{\"type\":\"result\",\"result\":{\"data\":{\"summary\":\"Exact cause: spaces are not filtered on the live PTY/rc path. The deterministic loss is terminal `Exit` overtaking queued final PTY output, followed by rc immediately terminating without unconditional VT restoration. Cold repaint’s omitted blank cells are intentional because it first emits ED2. Picker is a separate ratatui previous-buffer desynchronization, not a shared renderer.\",\"files\":[{\"path\":\"crates/spt-daemon/src/broker.rs\",\"description\":\"`OutputLog::append` lines 903-968 queues exact raw output; `controller_writer` lines 1675-1778 drains it asynchronously. The independent exit waiter lines 3757-3792 direct-writes `KIND_EXIT` through `all_sinks`, so Exit can overtake queued final output.\"},{\"path\":\"crates/spt-daemon/tests/broker.rs\",\"description\":\"Lines 376-403 explicitly state that EXIT can arrive before OUTPUT and make the test continue draining after Exit. Production rc does not continue.\"},{\"path\":\"crates/spt/src/rc.rs\",\"description\":\"`pump` lines 1966-1995 writes Output bytes verbatim but immediately returns on Exit. Cleanup lines 1254-1268 restores only the optional, currently-disabled status row and drops `RawGuard`; it emits no unconditional SGR reset, cursor show, scroll-region reset, or alternate-screen leave.\"},{\"path\":\"crates/spt-term/src/screen.rs\",\"description\":\"`render_repaint` lines 577-623 emits `ESC[0m ESC[2J ESC[H` before trimming trailing default spaces and skipping blank rows, so those omissions are correct. DECSTBM is tracked at lines 790-802 but never replayed, allowing later live scrolling to diverge.\"},{\"path\":\"crates/spt-term/src/reader.rs\",\"description\":\"`Drain::spawn` lines 118-161 forwards every byte read from the PTY; no whitespace or ANSI filtering.\"},{\"path\":\"crates/spt-daemon/src/attach.rs\",\"description\":\"Lines 600-609 base64-forward Output unchanged; lines 610-617 send Exit+FIN and return immediately, so late final Output cannot recover.\"},{\"path\":\"crates/spt/src/picker/mod.rs\",\"description\":\"Lines 165-178 invoke purge inline while ratatui still owns the alternate screen; this path does not use ScreenGrid or rc.\"},{\"path\":\"crates/spt/src/cli.rs\",\"description\":\"`cmd_endpoint_purge` lines 12707-12826 writes diagnostics/PURGED to stderr out-of-band, invalidating ratatui’s previous-buffer diff model.\"}],\"architecture\":\"Byte path is PTY -> `Drain::spawn` -> `OutputLog::append` -> `OutputEvent` base64 -> `Brain::read_event_until` -> `serve_attach` `AttachRecord::Output` -> loopback/QUIC -> rc decode -> `stdout.write_all`; it is byte-preserving. Reproduce the real race with a child that writes `XXXX ESC[2K ESC[?25h ESC[?1049l` and exits immediately: capture broker frame order; current code can emit Exit before that Output, and rc stops at Exit. Fix at source by making the drain/output writer FIFO the sole sequencer: observe drain EOF, then enqueue Exit behind all prior Output for controller and viewers; optionally add an Exit final-sequence watermark. Add rc display-state RAII that, on every exit/error/reconnect-give-up path and before Windows VT-output mode is restored, emits SGR reset, scroll-margin reset, cursor show, and leave-alt-screen. Replay tracked DECSTBM in cold repaint. Fix picker separately by preventing stderr writes while ratatui owns the screen or clearing/resetting ratatui’s previous buffer before the next draw.\"}}}","call_id":"call_R6vhGUoe7OaP3TapqVLS7TPa","name":"yield"}]},"duration":24023.064800024033,"ttft":1678.7589000165462,"contextSnapshot":{"promptTokens":218337,"nonMessageTokens":15525}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_R6vhGUoe7OaP3TapqVLS7TPa|fc_0d9d15bf8b62bf27016a5b42cafad481999290ca7539057938","toolName":"yield","startedAt":"2026-07-18T09:09:33.655Z"},"id":"577dd162","parentId":"29534c90","timestamp":"2026-07-18T09:09:33.655Z"}
{"type":"message","id":"28401dae","parentId":"577dd162","timestamp":"2026-07-18T09:09:33.655Z","message":{"role":"toolResult","toolCallId":"call_R6vhGUoe7OaP3TapqVLS7TPa|fc_0d9d15bf8b62bf27016a5b42cafad481999290ca7539057938","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Exact cause: spaces are not filtered on the live PTY/rc path. The deterministic loss is terminal `Exit` overtaking queued final PTY output, followed by rc immediately terminating without unconditional VT restoration. Cold repaint’s omitted blank cells are intentional because it first emits ED2. Picker is a separate ratatui previous-buffer desynchronization, not a shared renderer.","files":[{"path":"crates/spt-daemon/src/broker.rs","description":"`OutputLog::append` lines 903-968 queues exact raw output; `controller_writer` lines 1675-1778 drains it asynchronously. The independent exit waiter lines 3757-3792 direct-writes `KIND_EXIT` through `all_sinks`, so Exit can overtake queued final output."},{"path":"crates/spt-daemon/tests/broker.rs","description":"Lines 376-403 explicitly state that EXIT can arrive before OUTPUT and make the test continue draining after Exit. Production rc does not continue."},{"path":"crates/spt/src/rc.rs","description":"`pump` lines 1966-1995 writes Output bytes verbatim but immediately returns on Exit. Cleanup lines 1254-1268 restores only the optional, currently-disabled status row and drops `RawGuard`; it emits no unconditional SGR reset, cursor show, scroll-region reset, or alternate-screen leave."},{"path":"crates/spt-term/src/screen.rs","description":"`render_repaint` lines 577-623 emits `ESC[0m ESC[2J ESC[H` before trimming trailing default spaces and skipping blank rows, so those omissions are correct. DECSTBM is tracked at lines 790-802 but never replayed, allowing later live scrolling to diverge."},{"path":"crates/spt-term/src/reader.rs","description":"`Drain::spawn` lines 118-161 forwards every byte read from the PTY; no whitespace or ANSI filtering."},{"path":"crates/spt-daemon/src/attach.rs","description":"Lines 600-609 base64-forward Output unchanged; lines 610-617 send Exit+FIN and return immediately, so late final Output cannot recover."},{"path":"crates/spt/src/picker/mod.rs","description":"Lines 165-178 invoke purge inline while ratatui still owns the alternate screen; this path does not use ScreenGrid or rc."},{"path":"crates/spt/src/cli.rs","description":"`cmd_endpoint_purge` lines 12707-12826 writes diagnostics/PURGED to stderr out-of-band, invalidating ratatui’s previous-buffer diff model."}],"architecture":"Byte path is PTY -> `Drain::spawn` -> `OutputLog::append` -> `OutputEvent` base64 -> `Brain::read_event_until` -> `serve_attach` `AttachRecord::Output` -> loopback/QUIC -> rc decode -> `stdout.write_all`; it is byte-preserving. Reproduce the real race with a child that writes `XXXX ESC[2K ESC[?25h ESC[?1049l` and exits immediately: capture broker frame order; current code can emit Exit before that Output, and rc stops at Exit. Fix at source by making the drain/output writer FIFO the sole sequencer: observe drain EOF, then enqueue Exit behind all prior Output for controller and viewers; optionally add an Exit final-sequence watermark. Add rc display-state RAII that, on every exit/error/reconnect-give-up path and before Windows VT-output mode is restored, emits SGR reset, scroll-margin reset, cursor show, and leave-alt-screen. Replay tracked DECSTBM in cold repaint. Fix picker separately by preventing stderr writes while ratatui owns the screen or clearing/resetting ratatui’s previous buffer before the next draw."},"status":"success","type":"result"},"isError":false,"timestamp":1784365773655}}
{"type":"message","id":"7efff439","parentId":"28401dae","timestamp":"2026-07-18T09:09:35.351Z","message":{"role":"assistant","content":[{"type":"text","text":"","textSignature":"{\"v\":1,\"id\":\"msg_0d9d15bf8b62bf27016a5b42cf34388199b9634ec1141855f9\",\"phase\":\"final_answer\"}"}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":1584,"output":4,"cacheRead":217600,"cacheWrite":0,"totalTokens":219188,"cost":{"input":0.00792,"output":0.00012,"cacheRead":0.1088,"cacheWrite":0,"total":0.11684}},"stopReason":"stop","timestamp":1784365773697,"responseId":"resp_0d9d15bf8b62bf27016a5b42cde0448199a5d0a080197b70a7","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"msg_0d9d15bf8b62bf27016a5b42cf34388199b9634ec1141855f9","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"phase":"final_answer","role":"assistant"}]},"duration":1638.8328999876976,"ttft":1501.7737999856472,"contextSnapshot":{"promptTokens":219184,"nonMessageTokens":15525}}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-07-18T09:16:35.361Z"},"id":"17a22d9a","parentId":"7efff439","timestamp":"2026-07-18T09:16:35.361Z"}
