{"type":"title","v":1,"title":"","updatedAt":"2026-07-16T10:22:49.061Z","pad":"                                                                                                                                                                              "}
{"type":"session","version":3,"id":"019f6a73-4ceb-7001-9525-900000c164a5","timestamp":"2026-07-16T10:22:49.061Z","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core"}
{"type":"model_change","id":"9939c4f0","parentId":null,"timestamp":"2026-07-16T10:22:49.102Z","model":"openai-codex/gpt-5.6-sol"}
{"type":"thinking_level_change","id":"c49bd2bd","parentId":"9939c4f0","timestamp":"2026-07-16T10:22:49.102Z","thinkingLevel":"medium","configured":null}
{"type":"session_init","id":"51f75084","parentId":"c49bd2bd","timestamp":"2026-07-16T10:22:49.103Z","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: **Messaging Lifecycle Investigator**. Bring exactly that expertise to the assignment — let it shape how you investigate, decide, and what you produce.\n\nCONTEXT\n===================================\n\n# Goal\nDiagnose elevated endpoints failing `spt rc` and possibly messaging, distinguishing the hyphenated shortform-message issue.\n# Constraints\nRead-only investigation. Do not mutate live endpoints, refresh/update, send messages, edit files, or run broad test suites. Skip formatters/linters. Use CONTEXT.md, relevant ADRs/KNOWN-HAZARDS, and exact production call chains. Existing live endpoint `hertz` must remain untouched.\n# Contract\nReturn exact symbols/files, a minimal deterministic test/harness design, ranked falsifiable mechanisms, and what evidence distinguishes elevated behavior from ordinary endpoints.\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 `ElevatedMessaging`. 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, running)\n- `ElevatedRc` — Remote Control 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-16, and the current working directory is 'C:/Users/decid/Documents/projects/spt-core'.\n\n<critical>\n- Each response MUST advance the task. There is no stopping condition other than completion.\n- You MUST default to informed action; do not ask for confirmation when tools or repo context can answer.\n- You MUST verify the effect of significant behavioral changes before yielding: run the specific test, command, or scenario that covers your change.\n</critical>","task":"Complete the assignment below, thoroughly:\n\n# Target\nElevated endpoint message delivery lifecycle and listener/adapter attachment. Non-goal: CLI shortform grammar.\n# Change\nTrace endpoint elevation through registry, adapter/listener ownership, spool/drain, routing, and receive path. Determine whether elevated endpoints actually cannot receive messages or whether evidence points solely to target parsing. Compare ordinary/elevated endpoint state and find deterministic isolated integration-test seam.\n# Acceptance\nReport exact symbols/invariants, ranked hypotheses, and a minimal repro that can distinguish routing from parser conflation. No edits or live mutations.","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":"66fb5637","parentId":"51f75084","timestamp":"2026-07-16T10:22:49.111Z","message":{"role":"user","content":[{"type":"text","text":"Complete the assignment below, thoroughly:\n\n# Target\nElevated endpoint message delivery lifecycle and listener/adapter attachment. Non-goal: CLI shortform grammar.\n# Change\nTrace endpoint elevation through registry, adapter/listener ownership, spool/drain, routing, and receive path. Determine whether elevated endpoints actually cannot receive messages or whether evidence points solely to target parsing. Compare ordinary/elevated endpoint state and find deterministic isolated integration-test seam.\n# Acceptance\nReport exact symbols/invariants, ranked hypotheses, and a minimal repro that can distinguish routing from parser conflation. No edits or live mutations."}],"attribution":"agent","timestamp":1784197369104}}
{"type":"message","id":"d0e5cd25","parentId":"66fb5637","timestamp":"2026-07-16T10:22:56.774Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Locating CONTEXT ADR hazards and messaging**"},{"type":"toolCall","id":"call_6PrBp4LI35cnVm7oVLjxLF86|fc_017bff223290b7ba016a58b100c140819aac1a04458c3a2d86","name":"glob","arguments":{"i":"Mapping project documents","path":"CONTEXT.md; docs/**/*.md; src/**/*; test/**/*; tests/**/*","hidden":true,"gitignore":true,"limit":500}},{"type":"toolCall","id":"call_XEeMPJ4DDm8yThYL5hOXiqkw|fc_017bff223290b7ba016a58b100c158819aa88e6b52900a1c40","name":"grep","arguments":{"i":"Finding elevation lifecycle","pattern":"elevat|runas|admin|privileg|integrity","path":".","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_XwrIGddcBasIrtsnLX8Fqr7c|fc_017bff223290b7ba016a58b100c18c819aab61b213da78e46a","name":"grep","arguments":{"i":"Finding messaging lifecycle","pattern":"spool|drain|listener|adapter|receive|deliver|routing|registry","path":"src; test; tests; docs; CONTEXT.md","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Bz8DCw1gYLSjR3hbxSUGf8zU|fc_017bff223290b7ba016a58b100c19c819aae9219b27c97ef52","name":"grep","arguments":{"i":"Finding endpoint commands","pattern":"\\bspt rc\\b|remote control|shortform|endpoint|perch|target","path":"src; test; tests; docs; CONTEXT.md","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":14569,"output":284,"cacheRead":0,"cacheWrite":0,"totalTokens":14853,"reasoningTokens":25,"cost":{"input":0.07284500000000001,"output":0.00852,"cacheRead":0,"cacheWrite":0,"total":0.081365}},"stopReason":"toolUse","timestamp":1784197369158,"responseId":"resp_017bff223290b7ba016a58b0f9a4cc819a9dc8fd2c4e394ef4","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b0fba468819a996b4ec9adc81961","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLEAN6lC5XEKQXmNw9TShhNyw29XELDMp5VDVv2rKXi-szqj6HVkI5CZpPe4v--OduRvT9E3WB1wbkid34TxspCR1fmPWpHg1X3JnUdz1fY70-N9lBwcJoWP_RH64IcuKC5yJAEx_k_0GmM7uKedwvmgPcsRYTURRjtt37NPFHxggfWXSpecBFoHpTTDzdW3JAbE5xFVfDu5FLRMpjVECw8u31Fnhvgg9lU-mq1HFJId_s6WV_4ULqFUx5TAol9N_6_be-mqww1935hRiIXfJLgx3Pgqk_G3n40_fZ4cCPVgtYzaHKZENM92w3V4sOAQcEvCbuuA4gWhWp0toc36WEUUgCMVCJkoWdhvtA8dyIFHaVtoZvtcO2jo1lZIAdJH21PjztZIODnKqFBquZqeTaDXnJx__rnpOdnc0W4xffeFZy5FssdsZx6lGSH0i7ZLDnCD-TZ-VW5CptJUNlmh-qVdqEfku7twIPRJrtqrkIGEwmuPdpflJ2KNenigE50oBUnI5jDcV52yQMMnbkoLZB_XR3cCGE9Pn962bIBhKBrq8rhs0HtLrCM6_kUHGn2tKGljjM2lMBONqdngRHVd4CwBpycisC0e-P1UhNwq2eF9bNCBJ6V1TVHU1AnmXYGDomgOLbFkGu_hsxGHCHmb6lIcZfUSD3fBNQbDWDjCG0pfz6nOWIMwIlk4Lya5s1HKZMiKyjgEydnTqLvvWxY_uMuX2XW-u0o2rsQejSXCBPK2tH9eeRdk-_jfPoVaq2Jyu-mtAx864suwGwWPQuiOfOCafylUKRTSQSpT9RvWe_RpnmOj0LSYHGm2eWeZxNmZmOJCDQUONwf4euS12T3jDJj_cR6youHUtgGeLdLIiTwQe1IhbXxJzrYNr0MkC7BPLQyQrAPMZTyZfBhiHSbVtoVONJgZGb-uXxDGRW5uKZaYCFFnZs5x6a8utR1xp9xBddHJNZMUYgdD9jptEF-RIiRHnLG2LUTndB2o_z5Sb9WMYNeQtW5T1pmG8bV2p_dXivGg9z1XuV0zYc1h0VZ-R31X_nw5vnvtlZQp3ejHqeHc-28=","summary":[{"type":"summary_text","text":"**Locating CONTEXT ADR hazards and messaging**"}]},{"id":"fc_017bff223290b7ba016a58b100c140819aac1a04458c3a2d86","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping project documents\",\"path\":\"CONTEXT.md; docs/**/*.md; src/**/*; test/**/*; tests/**/*\",\"hidden\":true,\"gitignore\":true,\"limit\":500}","call_id":"call_6PrBp4LI35cnVm7oVLjxLF86","name":"glob"},{"id":"fc_017bff223290b7ba016a58b100c158819aa88e6b52900a1c40","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding elevation lifecycle\",\"pattern\":\"elevat|runas|admin|privileg|integrity\",\"path\":\".\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_XEeMPJ4DDm8yThYL5hOXiqkw","name":"grep"},{"id":"fc_017bff223290b7ba016a58b100c18c819aab61b213da78e46a","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding messaging lifecycle\",\"pattern\":\"spool|drain|listener|adapter|receive|deliver|routing|registry\",\"path\":\"src; test; tests; docs; CONTEXT.md\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_XwrIGddcBasIrtsnLX8Fqr7c","name":"grep"},{"id":"fc_017bff223290b7ba016a58b100c19c819aae9219b27c97ef52","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding endpoint commands\",\"pattern\":\"\\\\bspt rc\\\\b|remote control|shortform|endpoint|perch|target\",\"path\":\"src; test; tests; docs; CONTEXT.md\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_Bz8DCw1gYLSjR3hbxSUGf8zU","name":"grep"}]},"duration":7614.713400000706,"ttft":2456.7966000000015,"contextSnapshot":{"promptTokens":14569,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6PrBp4LI35cnVm7oVLjxLF86|fc_017bff223290b7ba016a58b100c140819aac1a04458c3a2d86","toolName":"glob","startedAt":"2026-07-16T10:22:56.774Z","args":{"path":"CONTEXT.md; docs/**/*.md; src/**/*; test/**/*; tests/**/*"},"intent":"Mapping project documents"},"id":"cc2827c5","parentId":"d0e5cd25","timestamp":"2026-07-16T10:22:56.774Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_XEeMPJ4DDm8yThYL5hOXiqkw|fc_017bff223290b7ba016a58b100c158819aa88e6b52900a1c40","toolName":"grep","startedAt":"2026-07-16T10:22:56.774Z","args":{"path":"."},"intent":"Finding elevation lifecycle"},"id":"8e2a8a03","parentId":"cc2827c5","timestamp":"2026-07-16T10:22:56.774Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_XwrIGddcBasIrtsnLX8Fqr7c|fc_017bff223290b7ba016a58b100c18c819aab61b213da78e46a","toolName":"grep","startedAt":"2026-07-16T10:22:56.774Z","args":{"path":"src; test; tests; docs; CONTEXT.md"},"intent":"Finding messaging lifecycle"},"id":"a95fe69c","parentId":"8e2a8a03","timestamp":"2026-07-16T10:22:56.774Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Bz8DCw1gYLSjR3hbxSUGf8zU|fc_017bff223290b7ba016a58b100c19c819aae9219b27c97ef52","toolName":"grep","startedAt":"2026-07-16T10:22:56.774Z","args":{"path":"src; test; tests; docs; CONTEXT.md"},"intent":"Finding endpoint commands"},"id":"32a25d12","parentId":"a95fe69c","timestamp":"2026-07-16T10:22:56.774Z"}
{"type":"message","id":"04bc51d7","parentId":"32a25d12","timestamp":"2026-07-16T10:22:56.790Z","message":{"role":"toolResult","toolCallId":"call_6PrBp4LI35cnVm7oVLjxLF86|fc_017bff223290b7ba016a58b100c140819aac1a04458c3a2d86","toolName":"glob","content":[{"type":"text","text":"CONTEXT.md\n# docs/\nKNOWN-HAZARDS.md\nREDISPATCH-TRUTH-TRIAGE.md\nSTORAGE.md\nPROJECT-INDEX-TRIAGE.md\nRELEASE-RUNBOOK.md\nTHE-FORKENING-TRIAGE.md\nUPDATE-WEDGE-2-ROUND3-CODEREAD.md\nUPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md\nPUMP-W2-DESIGN.md\nPUMP-W2-GATE-HANDOFF.md\nPUMP-W2-BUILD-DELTA.md\nPUMP-W2-BUILD-GO.md\nMSG-IDENTITY-DISPATCH.md\nGATEWAY-LIVENESS-DISPATCH.md\nGATEWAY-RCA-STEP1.md\nF-035-RCA.md\nF-035-RELAY-RCA.md\nF-035-CONFLICT.md\nPUMP-TRUTH-TRIAGE.md\nPUMP-W2-DISPATCH-RULING.md\nMUSL-RELEASE-HANDOFF.md\nMUSL-W3-DISPATCH-RULING.md\nMUSL-W1-DISPATCH-RULING.md\nNEXT-MILESTONE-MUSL-TIER-TRIAGE.md\nW6-GATE-VERDICT.md\nW6-DISPATCH-RULING.md\nW5-WAVE-GATE-REPORT.md\nW5-GATE-VERDICT.md\nW5-GATE-HOLD.md\nW5-DISPATCH-RULING.md\nW4-WAVE-GATE-REPORT.md\nW4-GATE-VERDICT.md\nW4-DISPATCH-RULING.md\nW3-WAVE-GATE-REPORT.md\nW3-GATE-VERDICT.md\nW3-SLICE1-REPORT.md\nW2-GATE-VERDICT.md\nW3-DIGEST-GENERATION-UNION-RULING.md\nW3-ENDPOINT-SURVIVAL-DESIGN.md\nW2-DESIGN-RULING.md\nW2-BUILT-REPORT.md\nW2-DESIGN-PROPOSAL.md\nNEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md\nINJECT-SETTLE-REARM-GATE-VERDICT.md\nNEXT-MILESTONE-WORKERTRUTH-TRIAGE.md\nFLAKE-LEDGER.md\nTWO-HOST-RUNBOOK.md\nNEXT-MILESTONE-REMOTETRUTH-TRIAGE.md\nMANIFEST.md\nFAULT-MATRIX.md\nCONTEXT-MEMORY.md\nF-030-PSYCHE-EPHEMERAL-DESIGN.md\nNEXT-MILESTONE-PICKER-POLISH-TRIAGE.md\nF-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\nNEXT-MILESTONE-RUNTRUTH-TRIAGE.md\nNEXT-MILESTONE-PICKER-TRIAGE.md\nNEXT-MILESTONE-BUG-TRIAGE.md\nDOCS-STRATEGY.md\nDEFERRED.md\nBROKER-BRAIN-SPLIT-RESTORATION.md\nDORMANCY-BUDGET.md\nDEBUG-CONVERGE-PLAN.md\nDEBUG-ROLLOUT.md\nTRACEABILITY.md\n## adr/\n0038-broker-stream-lifecycle-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\n0033-wake-resume-via-reconcile-intent.md\n0025-live-daemon-coordinated-adapter-update.md\n0022-spt-hosted-idle-delivery-translation-binary.md\n0032-boundary-rotation-credential.md\n0030-robust-wan-subnet-join.md\n0029-adapter-update-arc-resolve-not-execute.md\n0028-activity-gated-delivery-and-send-modifier-axes.md\n0027-unbound-endpoint-state-and-attach-on-session.md\n0026-multi-subnet-home-at-endpoint-run-creation.md\n0024-multi-platform-adapter-spt-packaging.md\n0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md\n0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md\n0020-event-envelope-sole-arriving-format-reply-to-removed.md\n0012-psyche-trust-boundary-stdout-event-outbound.md\n0009-endpoint-access-control-model.md\n0008-live-activity-buffer-pty-digest.md\n0019-digest-adapter-declared-extractor-session-spanning.md\n0018-broker-brain-process-isolation-restoration.md\n0016-platform-targeted-update-sets.md\n0017-subnet-mesh-membership-seed-proof.md\n0014-publish-target-repo-and-lapse-proof-canonical-url.md\n0015-release-signing-two-key-trust-anchor.md\n0013-psyche-reconciled-context-merge.md\n0010-immutable-home-subnet-fork-not-rehome.md\n0011-branchstore-git-kv-state-substrate.md\n0006-multi-subnet-membership-model.md\n0007-subnet-notification-primitive.md\n0003-multi-instance-node-anchored-identity-model.md\n0004-single-daemon-broker-brain-split-and-self-update.md\n0002-bake-networking-into-core-collapse-spt-node.md\n0001-clean-room-fork-from-claude-skill-owl.md\n## design/\nsubnet-presence-display.md\nrobust-wan-subnet-join.md\nv0.16.0-update-arc-and-cli.md\n## spikes/\nSPIKE-06-idempotent-boundary.md\nSPIKE-07-branchstore.md\nSPIKE-04-forkpty-parity.md\nSPIKE-05-restart-stress.md\nSPIKE-03-quic-survival.md\nSPIKE-02-iroh-smoke.md\nSPIKE-01-broker-handoff.md\n## reviews/\nSTAGE-A-codex-redteam.md\n# tests/\n.gitkeep\n# src/\n.gitkeep\n\nSkipped missing paths: test/**/*"}],"details":{"scopePath":"CONTEXT.md, docs/**/*.md, src/**/*, tests/**/*","fileCount":115,"files":["docs/KNOWN-HAZARDS.md","docs/adr/0038-broker-stream-lifecycle-truth.md","docs/REDISPATCH-TRUTH-TRIAGE.md","CONTEXT.md","docs/STORAGE.md","docs/adr/0037-materialized-project-index.md","docs/PROJECT-INDEX-TRIAGE.md","docs/RELEASE-RUNBOOK.md","docs/THE-FORKENING-TRIAGE.md","docs/adr/0036-private-fork-distribution.md","docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md","docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md","docs/PUMP-W2-DESIGN.md","docs/PUMP-W2-GATE-HANDOFF.md","docs/PUMP-W2-BUILD-DELTA.md","docs/PUMP-W2-BUILD-GO.md","docs/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","docs/PUMP-TRUTH-TRIAGE.md","docs/PUMP-W2-DISPATCH-RULING.md","docs/MUSL-RELEASE-HANDOFF.md","docs/MUSL-W3-DISPATCH-RULING.md","docs/MUSL-W1-DISPATCH-RULING.md","docs/NEXT-MILESTONE-MUSL-TIER-TRIAGE.md","docs/W6-GATE-VERDICT.md","docs/W6-DISPATCH-RULING.md","docs/W5-WAVE-GATE-REPORT.md","docs/W5-GATE-VERDICT.md","docs/W5-GATE-HOLD.md","docs/W5-DISPATCH-RULING.md","docs/W4-WAVE-GATE-REPORT.md","docs/W4-GATE-VERDICT.md","docs/W4-DISPATCH-RULING.md","docs/W3-WAVE-GATE-REPORT.md","docs/W3-GATE-VERDICT.md","docs/W3-SLICE1-REPORT.md","docs/W2-GATE-VERDICT.md","docs/W3-DIGEST-GENERATION-UNION-RULING.md","docs/W3-ENDPOINT-SURVIVAL-DESIGN.md","docs/W2-DESIGN-RULING.md","docs/W2-BUILT-REPORT.md","docs/W2-DESIGN-PROPOSAL.md","docs/NEXT-MILESTONE-LIFECYCLETRUTH-TRIAGE.md","docs/INJECT-SETTLE-REARM-GATE-VERDICT.md","docs/adr/0005-totp-seeded-spake2-node-pairing.md","docs/NEXT-MILESTONE-WORKERTRUTH-TRIAGE.md","docs/FLAKE-LEDGER.md","docs/adr/0031-server-side-screen-grid-render-repaint.md","docs/adr/0034-per-minter-op-id-namespacing.md","docs/TWO-HOST-RUNBOOK.md","docs/NEXT-MILESTONE-REMOTETRUTH-TRIAGE.md","docs/MANIFEST.md","docs/FAULT-MATRIX.md","docs/adr/0033-wake-resume-via-reconcile-intent.md","docs/adr/0025-live-daemon-coordinated-adapter-update.md","docs/CONTEXT-MEMORY.md","docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md","docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md","docs/NEXT-MILESTONE-PICKER-POLISH-TRIAGE.md","docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","docs/NEXT-MILESTONE-RUNTRUTH-TRIAGE.md","docs/adr/0032-boundary-rotation-credential.md","docs/NEXT-MILESTONE-PICKER-TRIAGE.md","docs/NEXT-MILESTONE-BUG-TRIAGE.md","docs/design/subnet-presence-display.md","docs/adr/0030-robust-wan-subnet-join.md","docs/design/robust-wan-subnet-join.md","docs/design/v0.16.0-update-arc-and-cli.md","docs/adr/0029-adapter-update-arc-resolve-not-execute.md","docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md","docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md","docs/adr/0026-multi-subnet-home-at-endpoint-run-creation.md","docs/adr/0024-multi-platform-adapter-spt-packaging.md","docs/DOCS-STRATEGY.md","docs/DEFERRED.md","docs/adr/0023-always-on-endpoints-resident-supervised-binary-sigil-addressing.md","docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md","docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md","docs/adr/0012-psyche-trust-boundary-stdout-event-outbound.md","docs/adr/0009-endpoint-access-control-model.md","docs/adr/0008-live-activity-buffer-pty-digest.md","docs/adr/0019-digest-adapter-declared-extractor-session-spanning.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","docs/BROKER-BRAIN-SPLIT-RESTORATION.md","docs/spikes/SPIKE-06-idempotent-boundary.md","docs/spikes/SPIKE-07-branchstore.md","docs/spikes/SPIKE-04-forkpty-parity.md","docs/spikes/SPIKE-05-restart-stress.md","docs/spikes/SPIKE-03-quic-survival.md","docs/adr/0016-platform-targeted-update-sets.md","docs/adr/0017-subnet-mesh-membership-seed-proof.md","docs/adr/0014-publish-target-repo-and-lapse-proof-canonical-url.md","docs/adr/0015-release-signing-two-key-trust-anchor.md","docs/adr/0013-psyche-reconciled-context-merge.md","docs/adr/0010-immutable-home-subnet-fork-not-rehome.md","docs/adr/0011-branchstore-git-kv-state-substrate.md","docs/adr/0006-multi-subnet-membership-model.md","docs/adr/0007-subnet-notification-primitive.md","docs/adr/0003-multi-instance-node-anchored-identity-model.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0002-bake-networking-into-core-collapse-spt-node.md","docs/DORMANCY-BUDGET.md","docs/DEBUG-CONVERGE-PLAN.md","docs/DEBUG-ROLLOUT.md","docs/TRACEABILITY.md","docs/spikes/SPIKE-02-iroh-smoke.md","tests/.gitkeep","src/.gitkeep","docs/spikes/SPIKE-01-broker-handoff.md","docs/reviews/STAGE-A-codex-redteam.md","docs/adr/0001-clean-room-fork-from-claude-skill-owl.md"],"truncated":false,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","missingPaths":["test/**/*"]},"isError":false,"timestamp":1784197376790}}
{"type":"message","id":"3f457b97","parentId":"04bc51d7","timestamp":"2026-07-16T10:22:56.806Z","message":{"role":"toolResult","toolCallId":"call_XEeMPJ4DDm8yThYL5hOXiqkw|fc_017bff223290b7ba016a58b100c158819aa88e6b52900a1c40","toolName":"grep","content":[{"type":"text","text":"# CHANGELOG.md\n 348|\n*349|A join-truth release: joining a subnet now tolerates a skewed or stepped system clock so it finds members it used to silently miss, no longer raises the elevated-permission prompt before a member is actually found, and can show the exact ceremony clock it is using — plus a loud warning when no time server can be reached.\n 350|\n 351|### Added\n 352|\n...\n 357|\n*358|- **Joining no longer raises the elevated-permission prompt before a member is found.** `spt subnet join` now asks for OS elevation only once it has actually located a subnet member, rather than up front — so a join that can't find anyone no longer pops an elevation prompt for nothing. The `--code` path is unchanged.\n 359|\n 360|### Fixed\n 361|\n...\n 919|- **`spt subnet join` shows a QR code + setup code on success.** After joining, scan the QR (or read the `otpauth://` code) to re-provision an authenticator app for the subnet.\n*920|- **Privilege-gated commands self-elevate, cross-platform.** When a command needs elevation, spt re-launches itself the right way for your system — a Windows UAC prompt, a Linux desktop `pkexec`/terminal `sudo`, or inline `sudo` in a terminal — and prints the exact command to run by hand when it can't.\n 921|- `spt spt` — ???\n 922|\n 923|### Changed\n...\n 1001|\n*1002|- **`spt subnet revoke <node>…`** — remove one or more machines from a subnet across the **whole fleet**, not just locally. It tells every member to drop the node within moments, then rotates the subnet's shared secret so the removed machine is locked out and must re-pair to come back. By default the rotation is batched at the end of a one-hour window — several revokes in that window share a single rotation, and any member that was briefly offline heals automatically across it. Pass `--force-rotate...\n 1003|- **`spt daemon start`** — bring the daemon up in the background, idempotently. When `spt` is installed as a service (the Linux per-user service, or the Windows logon task), `start` and `stop` now drive *that* service instead of a stray hand-started daemon — so the two never fight each other for the connection. `spt daemon start` on an already-running daemon just says so and does nothing.\n 1004|\n 1005|### Changed\n...\n 1009|- **`spt daemon stop` is service-aware.** If a managed service owns the daemon, `stop` asks the service manager to stop it cleanly (so it doesn't immediately restart), instead of signalling the process directly. A hand-started daemon still stops the same way as before.\n*1010|- **`spt daemon run` is now strictly foreground on every platform** — it stays attached to your terminal until you stop it (the form the installed service uses). For a background daemon, use `spt daemon start`. On Windows, running `daemon run` from an elevated shell now refuses with a hint rather than silently disappearing into the background.\n 1011|- **`spt daemon status` shows what manages the daemon** — whether a service owns it (and is active) or it was started by hand.\n 1012|\n 1013|### Performance\n...\n 1044|- **Automatic re-pair cleanup.** If you reinstall or regenerate a node's identity and pair it again from the same machine under the same name, its old, now-dead identity is removed automatically during pairing — no manual prune needed.\n*1045|- **Firewall setup on Windows.** The installer (when run elevated) now adds the inbound network rule `spt` needs so other nodes can reach you. If it wasn't added, `spt subnet status` and the \"coming online\" banner now tell you the rule is missing and print the exact command to add it.\n 1046|- **Starts on boot.** The installer now registers `spt` to start automatically — at login on Windows, and as a per-user service on Linux — so your node is reachable after a reboot without running a command first.\n 1047|\n 1048|### Changed\n...\n 1066|- **Messages from other agents now arrive properly formatted.** Incoming messages on the listener stream now include the full envelope with the sender's name, instead of showing as a raw, unwrapped line.\n*1067|- **`sudo spt …` now works on Linux user installs.** When `spt` is installed to your user directory, elevation guidance that said to \"run as administrator/root\" used to dead-end with `sudo: spt: command not found`. The installer now also makes `spt` reachable under `sudo`, and on an interactive terminal `spt` re-runs itself with `sudo` automatically; otherwise it prints a command that actually works.\n*1068|- **Elevated `spt` on Linux runs under your account, not root.** The first time you run an elevated `spt`, it asks once which account should own the daemon and its data, remembers that choice, and every later `sudo spt` runs the daemon and stores state under that account — never as root.\n*1069|- **`spt daemon stop` on Windows now finds the daemon it started.** A daemon launched through Windows' elevation prompt could end up using the wrong home directory, so `spt daemon stop` reported \"daemon not running\" while a daemon kept running. It now keeps the right home directory across that elevation step.\n*1070|- **Removed a confusing internal status line.** `spt` no longer prints the internal \"DEELEVATED: running as uid …\" notice during normal use.\n 1071|- **Stale node rows clear out on their own.** Nodes that haven't been heard from in a while are now removed from the listing automatically, so old/dead entries stop cluttering `spt subnet status`.\n 1072|\n 1073|## [0.1.1] - 2026-06-07\n\n# CI-SELFHOST-PLAN.md\n 21|\n*22|1. **Registration tokens** (repo admin):\n 23|   `gh api -X POST repos/SaberMage/spt-core/actions/runners/registration-token --jq .token`\n 24|2. **HFENDULEAM runner**: download actions-runner (win-x64), `config.cmd --url\n 25|   https://github.com/SaberMage/spt-core --token <T> --labels hfenduleam\n\n# CONTEXT.md\n 727|**seed rotation / revoke**:\n*728|Removing a member is **`spt subnet revoke <node>...`** (elevation-gated, revoke-only — *adding* a member never rotates; the joiner just receives the current seed at pairing). Two effects:\n 729|- **Immediate:** writes a **roster tombstone** per revoked pubkey (see *member roster*) — propagates over member connections, suppresses that pubkey under the roster's union-merge, and augments the inbound gate to **membership-proof ∧ ¬tombstoned** so the node can't reconnect-and-reinsert (it still holds the seed until rotation). Force-drops its connections.\n 730|- **Coalesced rotation:** the tombstone schedules **one** seed rotation (re-mint seed, bump the **seed-rotation epoch** — `SubnetRecord.epoch`, ADR-0005 #10; push the new seed confidentially over member-authenticated TLS connections, **never** in roster/registry gossip — the seed is not roster data) at the close of a **coalescing window (default 1 h)**. Further revokes within the window join the same rotation → **one epoch bump** however many nodes, keeping benign offliners inside the single-epoch ...\n 731|A completed re-pair ceremony for a tombstoned pubkey clears its tombstone (deliberate re-admit).\n...\n 766|\n*767|**Node-bound code fetch — and every subnet-membership mutation — is gated behind OS privilege elevation** (Windows UAC / Linux root-or-equivalent): retrieving a subnet's code *from the node*, minting a new subnet (`subnet create` — a seed reveal), and joining one (`subnet join` — enrolling the machine into a trust fabric) all require either hardware/elevated access OR an **elevated endpoint** (an agent whose process is elevated can surface it). The join gate exists because membership is a trust-b...\n 768|\n*769|**self-elevating re-launch (cross-platform)**:\n*770|<!-- [doc->REQ-ELEVATE-1] -->\n*771|When a gated command is run unelevated, spt does not just print \"run as administrator\" — it **re-launches itself with privilege** so the user reaches the result in one step. The path is chosen by a pure decision seam (`elevation::decide_elevation_path`) from the OS, the current elevation, and the environment: an **interactive Unix TTY** re-execs inline under `sudo`; a **Linux desktop without a TTY** prefers **`pkexec`** (native polkit GUI auth, clean stdio) and falls back to a **terminal-emulator** (`x...\n 772|\n 773|**node label**:\n 774|A human display name for a node, defaulting to the machine's OS hostname (re-checked at daemon startup; a hostname change updates the label). Advertised through the existing registry gossip so subnet views render `HFENDULEAM (bcead52b…)` instead of bare key hex. The pubkey remains the identity; the label is **addressable**: an `@node` qualifier accepts a label or a key-prefix (`ling@hfenduleam`). Labels are not unique — an ambiguous label follows the resolution policy's refuse-and-qualify rule (refus...\n...\n 832|**arriving-message envelope**:\n*833|Every message that arrives at a consumer — an agent over `api listen`/`spt ready`, an adapter's hook composer over `api poll`/`api worker-poll`, the cross-node WAN feed — is delivered as the canonical **`<EVENT type=\"…\" from=\"…\">body</EVENT>` envelope** (`spt-proto::event`, the ADR-0001 grammar; self-delimiting, `from` attribution, `<br>` body escaping, `<EVENT-PART>` chunking). It is the one format adapters parse on, identical across every **arriving-message** surface on an agent perch. The earl...\n 834|\n 835|**file-transfer progress**:\n 836|Every file transfer over the substrate is addressable and **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI) — at any point during the transfer. A substrate-wide requirement, not shell-specific (the Shell text+file channel inherits it).\n\n# DAEMON-LIFECYCLE-PLAN.md\n 29|- `crates/spt-daemon/src/daemon.rs`: `ensure_running()` → `spawn_detached()`\n*30|  (Linux fork-style; Windows `DETACHED_PROCESS | CREATE_NO_WINDOW`, de-elevated\n*31|  via `deelevate`). `request_stop()` = IPC stop.\n 32|- Installer-registered services:\n 33|  - Linux (`install.sh`): systemd **user** unit `~/.config/systemd/user/spt-daemon.service`,\n 34|    `ExecStart=<bin> daemon run`, `enabled`; `loginctl enable-linger` for pre-login boot.\n 35|  - Windows (`install.ps1`): at-logon **scheduled task** `spt-core daemon`\n*36|    (`schtasks /SC ONLOGON /TR \"<bin> daemon run\"`) — needs elevation to register.\n 37|\n 38|## Target model (consistent across platforms)\n 39|\n...\n 68|  - (a) **Promote to a real Windows Service** (`sc.exe create` / a small service\n*69|    host) → clean `start`/`stop` parity with systemd. Heavier; needs elevation\n 70|    + a service-mode entry in the binary.\n 71|  - (b) **Keep the scheduled task for boot-start; `start` = detached spawn,\n 72|    `stop` = IPC stop.** Simpler; honest asymmetry (Windows has no per-user\n...\n 84|- Audit why Windows `spt daemon run` goes invisible-background (likely the\n*85|  `Daemon::run()` de-elevation respawn / a `spawn_detached` on the `run` path).\n 86|  Make `run` strictly foreground on Windows — the detached/invisible behavior\n 87|  lives ONLY in `start`. Keep `CREATE_NO_WINDOW` for the `start` detached spawn,\n 88|  not for foreground `run`.\n...\n 111|- v0.2.0 already registered the systemd unit on kitsubito + the scheduled-task\n*112|  attempt on HFENDULEAM (HFENDULEAM's task needs an elevated re-run — still open).\n 113|- After this ships: re-roll to the fleet so `start`/`stop` drive the managed\n 114|  service everywhere; document the daemon-lifecycle UX in the CLI reference +\n 115|  CHANGELOG.\n\n# DAEMON-LIFECYCLE-VERIFICATION.md\n 49|\n*50|- REQ-DAEMON-7's elevated-`run`-refuses behavior is Windows-only (N/A on this\n 51|  rig); Linux `daemon run` is foreground by construction (the in-process\n*52|  privilege drop keeps it foreground), which the unit's `ExecStart` relies on.\n 53|- The internal `ensure_running` auto-start shares the exact `plan_start(false,\n 54|  detected) → ServiceStart → systemctl --user start` path proven in step 3; a\n 55|  dedicated CLI trigger was skipped to avoid mutating live node state.\n\n# M12-CANDIDATES.md\n 18|\n*19|2. **Subnet create/join QR + self-elevating window.** `spt subnet create/join/show-totp`:\n 20|   - Surfaces a **QR code** of the TOTP seed for the user to store → must spawn a\n 21|     window to display it (create AND join).\n 22|   - Behavior change (per operator sketch): runs normally if env is interactive +\n*23|     elevated; ELSE re-runs self in a self-elevating window with inline stdout\n*24|     (\"Elevated terminal launched. Accept the prompt and proceed there.\" → \"You can\n 25|     close this window.\", no auto-close).\n*26|   - Cross-platform elevation fallbacks: Windows = UAC self-elevate window;\n 27|     Linux+desktop = pkexec/polkit or x-terminal-emulator; Linux+TTY = inline sudo;\n 28|     headless/no-TTY = print exact command for the human (agent relays).\n*29|   - Scope elevation to only steps that need it (service-install / firewall /\n*30|     privileged-port) — subnet-create itself likely unprivileged.\n 31|   - Complementary downstream: spt-claude-code skill with create/join/show-code verbs.\n 32|\n 33|3. **spt-hosted HARNESS bringup + local user PTY attach (CLI). — CONFIRMED MISSING, TOP PRIORITY.**\n\n# M12-PLAN.md\n 56|\n*57|## Wave 4 — subnet UX (QR + self-elevation)\n 58|- **T4.1** `spt subnet create/join/show-totp`: surface a **QR code** of the TOTP seed (window\n 59|  on create AND join).\n*60|- **T4.2** Self-elevating behavior: run normally if interactive+elevated; else re-run self in\n*61|  a self-elevating window with inline stdout (\"Elevated terminal launched… / You can close\n 62|  this window.\"). Cross-platform: Win UAC window · Linux+desktop pkexec/polkit or\n 63|  x-terminal-emulator · Linux+TTY inline sudo · headless/no-TTY print command for the human.\n*64|  Scope elevation to only steps that need it (service/firewall/privileged-port; subnet-create\n*65|  itself likely unprivileged).\n 66|\n 67|## Wave 5 — whoami merge (small)\n 68|- **T5.1** `spt whoami` → alias for `spt endpoint list`; SELF-pin output additionally includes\n...\n 73|  cross-node attach proven; file-backed strings + adapter:profile fallback work; subnet QR +\n*74|  elevation on Win+Linux; whoami alias. Full suite green; `traceable-reqs check` EXIT=0;\n 75|  cross-subnet/PTY proof demonstrated (this is the spt-claude-code v1 acceptance dependency).\n 76|- Then unblock perri to begin `spt-claude-code` (see `../spt-claude-code/SCOPE.md`).\n\n# M12-W2.5-RULING.md\n 57|- Gate: `access_check(endpoint, origin, Unsolicited)` already decides who may control; taking\n*58|  control is the same gate (no elevated kick policy — if you may drive, you may take).\n 59|\n 60|### 6. Picker (W2 picker integration)\n 61|blue ■ = a controller is present (`driven_by` = controller node) **+ a viewer count** (\"controlled\n\n# M12-W4-PLAN.md\n*1|# M12 Wave 4 — subnet QR + self-elevating window — JIT plan\n 2|\n 3|> todlando 2026-06-14, after the W2 + follow-on batch gate (PASS, committed\n*4|> 6e76f62 / eb74b18). doyle: \"proceed to W4 subnet QR + self-elevating window;\n 5|> ping a design-check JIT like the others.\" **DESIGN-CHECK PENDING — build only\n*6|> after doyle's ruling.** The load-bearing unknown is T4.2 (self-elevating\n*7|> *window*): cross-platform + a security surface (spawning elevated processes /\n 8|> UAC). Spec: `M12-PLAN.md` Wave 4 + `M12-CANDIDATES.md`.\n 9|\n 10|## Scope (M12-PLAN.md Wave 4)\n...\n 12|  seed (on create AND join).\n*13|- **T4.2** **Self-elevating behavior**: run normally if interactive + elevated;\n*14|  else re-run self in a self-elevating window with inline stdout (\"Elevated\n 15|  terminal launched… / You can close this window\"). Cross-platform — Win UAC\n 16|  window · Linux+desktop pkexec/polkit or x-terminal-emulator · Linux+TTY inline\n*17|  sudo · headless/no-TTY print the command for the human. Scope elevation to only\n*18|  the steps that need it (service/firewall/privileged-port; subnet-create itself\n*19|  is unprivileged-to-run but reveals a secret, so it is gated).\n 20|\n 21|## What EXISTS today (investigation 2026-06-14)\n 22|- **QR already renders** (M7/M8, REQ-SUBNET-1): `render_qr` (cli.rs:2964) encodes\n...\n 26|  shipped** — the open question is whether anything is actually missing (Q1).\n*27|- **Elevation is PARTIALLY built** (`spt/src/elevation.rs`, REQ-PAIR-6 /\n 28|  REQ-HAZARD-SUDO-SECURE-PATH):\n 29|  - `current()` probe (Win token / Unix euid, fail-safe `Unknown`); `gate_for`\n 30|    (Show/Fallback); `fallback_message`.\n*31|  - `current_style()` (Unix `Sudo` / Win `AdminTerminal`); `rerun_command`\n*32|    (absolute-path sudo line, secure_path-proof); `should_auto_elevate` (Unix ∧\n*33|    interactive ∧ NotElevated).\n*34|  - `cli.rs::try_auto_elevate` ACTUALLY re-execs `sudo <abs-exe> <args>` on an\n*35|    **interactive Unix TTY** (the elevated child does the work; `main` de-elevates\n*36|    back via the 5.7 drop). `with_elevation_hint` prints the absolute-path sudo\n 37|    command otherwise. Consumed by `cmd_subnet_create` + `cmd_subnet_show_code`.\n 38|- **So the W4 T4.2 DELTA is the rest of the matrix:** today only **Unix +\n*39|  interactive-TTY** auto-elevates (inline sudo); **Windows prints \"run as\n*40|  administrator\" with NO actual UAC re-launch**; **Linux desktop without a TTY**\n 41|  (DISPLAY but no terminal) has no path; **headless** falls to the printed hint\n 42|  (Unix only). T4.2 = add the **Windows UAC window**, the **Linux desktop\n 43|  pkexec/terminal-emulator** path, and a **headless print** that covers Windows too.\n*44|- De-elevation seam exists (`spt-daemon::deelevate`, REQ-HAZARD-ELEVATED-DAEMON-SPAWN)\n*45|  — the elevated child drops back to the user for state safety; the\n\n# M12-W4-RULING.md\n*1|# M12 W4 — doyle design ruling (subnet QR + self-elevation)\n 2|\n*3|> doyle 2026-06-14, gating todlando's W4 JIT (M12-W4-PLAN.md). Privilege-escalation feature →\n 4|> security conditions stated explicitly + a mandatory hazard REQ. Primitives verified vs source\n 5|> before ruling.\n 6|\n 7|## Verified against source\n*8|- `elevation::rerun_command` enforces the **absolute exe path** (secure_path-proof, KH 5.10) — test `sudo_rerun_uses_absolute_path_not_bare_name` asserts `starts_with(\"sudo /\")`.\n*9|- `try_auto_elevate` (cli.rs:3065) re-execs `sudo <current_exe_str> <invocation_args>` — **verbatim args**, no widening.\n*10|- `ELEVATION_PROVEN` flag + `current()` (elevation.rs:164) short-circuit to `Elevated` once proven → **loop-guard foundation exists**.\n 11|- `render_qr` (cli.rs:2964) renders the `otpauth://` URI on create / show-code / join. T4.1 largely shipped.\n 12|\n 13|The matrix builds on sound primitives. Rulings:\n...\n 15|## Q1 — T4.1 QR: verify + close the join gap; \"window\" = inline terminal QR, NO GUI. ✓\n*16|Confirm `cmd_subnet_join` renders the QR (not only the code) post-join; add the `render_qr` call if missing, else no-op. No GUI milestone exists — reading \"window\" as a GUI surface would invent scope. (The \"window\" in the *milestone title* belongs to T4.2's elevated console, not a QR surface.)\n 17|\n*18|## Q2 — Windows UAC child-window: `runas` on the abs-exe DIRECTLY, child self-pauses, NO stdout marshaling.\n*19|`ShellExecuteW` with the `runas` verb on **`<abs-exe> <verbatim-args>`** — **not** a `cmd /k` wrapper (a `cmd /k` leaves an interactive *privileged shell* open = needless escalated surface). The elevated child detects it is the elevated re-launch, does the work, prints the result + \"You can close this window,\" and **pauses for a keypress** so the user reads it. The original process prints \"Elevated terminal launched…\" and exits 0.\n*20|- **Security (binding): never pipe / capture the elevated child's stdout back across the privilege boundary.** The unprivileged parent must not depend on privileged output. The child is self-contained.\n 21|- Verbatim args + absolute exe path (same secure-path discipline as the sudo leg, applied to the Windows launcher).\n 22|\n 23|## Q3 — Linux desktop order: confirmed; argv-array, never a shell string.\n 24|interactive-TTY → inline `sudo` (existing) → else `DISPLAY` ∧ `pkexec` → `pkexec` → else `DISPLAY` ∧ terminal-emulator → `x-terminal-emulator -e sudo …` (fallback list: x-terminal-emulator → gnome-terminal → konsole → xterm) → else → print the absolute-path sudo hint. pkexec preferred (native polkit GUI auth, clean stdio, no extra window).\n*25|- **Security (binding): pass argv as an ARRAY to pkexec / the terminal-emulator / runas — never a shell-interpolated command string.** A crafted id / path / arg must not be able to inject a second command. (`Command::new(...).args([...])`, not `sh -c \"<interpolated>\"`.)\n 26|\n 27|## Q4 — pure decision seam: APPROVED.\n*28|`decide_elevation_path(os, elevation, interactive_tty, has_display, has_pkexec, has_term_emulator) -> ElevatePath { AlreadyElevated, InlineSudo, UacWindow, Pkexec, TerminalEmulator, PrintHint }`, pure + unit-tested across the matrix; each variant's actual launch is the impure manual-verify leg (REQ-PAIR-6 precedent). Generalizes `should_auto_elevate`.\n*29|- **Required matrix coverage: `AlreadyElevated` returned whenever `current()==Elevated` (or `ELEVATION_PROVEN`), on EVERY os** — this is the loop-safety guarantee expressed in the testable seam. The elevated child re-enters, sees `Elevated`, and never re-elevates.\n 30|\n*31|## Q5 — REQ granularity: NEW REQ-ELEVATE-1. ✓\n*32|Cross-platform self-elevating re-launch is a distinct mechanism reused by every gated command (not subnet-specific). New **REQ-ELEVATE-1** (decision matrix + per-OS window launch + inline-notice UX). doc+impl+unit, **no int** (launch is manual-verify).\n 33|\n 34|## Q6 — Security posture: YES, add a hazard REQ. NON-NEGOTIABLE.\n*35|A privilege-escalation feature MUST carry a `REQ-HAZARD-*` with conformance tests. Add **REQ-HAZARD-SELF-ELEVATE** (required: unit; the hazard = a self-elevation that widens scope, hijacks PATH, injects via a shell string, or loops). The invariant:\n 36|\n*37|> Self-elevation re-runs the EXACT original invocation with the **absolute exe path** — never widening privilege scope, never adding/altering args, never via a PATH-resolved bare name, never via a shell-interpolated string (argv-array only); the elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates** (loop-safe via `AlreadyElevated`).\n 38|\n 39|Unit conformance (extends the existing abs-path test across the new launchers):\n*40|1. Every launcher's command uses the absolute exe path + verbatim args (Win `runas`, pkexec, terminal-emulator, sudo).\n*41|2. `decide_elevation_path` → `AlreadyElevated` when elevated, every os (loop-safety).\n 42|3. Launch argv is an array, not a shell string (no-injection — assert the constructed argv, no `sh -c`).\n 43|\n*44|The existing REQ-HAZARD-SUDO-SECURE-PATH covers the Unix abs-path-under-sudo facet; REQ-HAZARD-SELF-ELEVATE covers the cross-platform verbatim / no-widen / no-shell-injection / loop-safe facets. The \"print hint\" floor must also print the **absolute-path** command (copy-paste stays secure_path-proof).\n 45|\n 46|## Scope confirmation\n 47|W4 is mostly T4.2's Windows + Linux-desktop launch arms over the existing pure-seam pattern — smaller than W2. Agreed.\n...\n 49|## Binding gate conditions (I verify all three)\n*50|1. **Loop-safety** proven in the pure seam: `AlreadyElevated` short-circuit on every os.\n\n# M4-D2-WIRE-PLAN.md\n 109|D2f (multi-subnet ceremony inputs: code+name, rendezvous token, create-new names up front —\n*110|REQ-PAIR-4/5) → D2g (elevation-gated `spt pair show-totp` — REQ-PAIR-3/6) → D3 registry.\n\n# M4-D2f-PLAN.md\n 59|  library mechanism (create_subnet + named ceremony + seed transfer) is what D2f delivers.\n*60|- Elevation-gated code fetch (`spt pair show-totp`) → D2g (REQ-PAIR-6, REQ-PAIR-3).\n 61|\n 62|## Conventions (carried)\n 63|- NO `cargo fmt`. Tag `[impl->REQ-PAIR-{4,5}]` on real evidence, `[unit->…]` on tests.\n\n# M4-D2g-PLAN.md\n*1|# M4-D2g — elevation-gated per-subnet code fetch (JIT plan)\n 2|\n 3|**Status:** not started. D2f (multi-subnet + seed transfer) shipped & CI-green at `452358d`;\n 4|REQ-PAIR-4/5 `[impl,unit]`. D2g is the LAST D2 task: the local `spt pair show-totp` command\n*5|that surfaces a subnet's current code, **gated behind OS privilege elevation** (REQ-PAIR-6),\n 6|fetchable per-subnet from any paired node (REQ-PAIR-3). Local-only — no daemon/P2P, so\n 7|buildable now without the D4 net wiring.\n 8|\n...\n 11|`spt pair show-totp [--subnet <name> | --create-new]` prints the subnet's current 6-digit\n*12|code + `otpauth://` URI **only when the process is elevated** (Windows UAC / Linux\n 13|root-or-equiv); otherwise it refuses the node-bound fetch and points the user to their\n 14|authenticator app (where the seed was provisioned at pairing). Per-subnet because a node\n 15|holds N seeds (ADR-0006); `--create-new` mints+names a fresh subnet (sole seed-holder).\n...\n 18|\n*19|- **Real elevation gate** (todlando is available for admin-enabled testing on this machine).\n*20|  Linux: `libc::geteuid() == 0`. Windows: `GetTokenInformation(.., TokenElevation, ..)` via\n 21|  the `windows` crate. `cfg`-split; an unknown/unsupported platform returns `Unknown` →\n*22|  treated as NOT elevated (fail safe → app fallback). Keep deps leaf-local (the elevation\n 23|  util crate only), preserving the dependency-minimal core.\n*24|- **Where:** put `is_elevated()` / `Elevation` in the `spt` binary (CLI-adjacent, e.g.\n*25|  `crates/spt/src/elevation.rs`) — it gates a CLI command, and only the binary need pull the\n 26|  OS deps. The code fetch itself reads `spt-store::SubnetStore` (already there).\n 27|- **No-flag behavior:** if the node holds exactly one subnet, use it; if several, **refuse +\n 28|  require `--subnet`** (no guessing — same ethos as D3c addressing). Zero subnets → tell the\n...\n 34|\n*35|1. **`elevation.rs`** (REQ-PAIR-6) — `enum Elevation { Elevated, NotElevated, Unknown }` +\n*36|   `fn current() -> Elevation`. `#[cfg(unix)]` libc geteuid; `#[cfg(windows)]` windows-crate\n*37|   TokenElevation; else Unknown. Unit-test the mapping logic where possible (the OS call\n 38|   itself is environment-dependent — gate behind a thin testable seam: a function taking the\n*39|   raw elevated-bool and producing the gate decision + message).\n 40|2. **`Cmd::Pair { ShowTotp { subnet, create_new } }`** in `cli.rs` — clap subcommand, stable\n 41|   arg shapes (REQ-MSG-2 style), parse tests.\n 42|3. **Handler** (REQ-PAIR-3) — load SubnetStore; resolve subnet (flag / single / refuse);\n*43|   `--create-new` → `create_subnet`; gate on `Elevation::current()`; elevated → print code\n 44|   (`seed.code_at(now)`) + `otpauth_uri`; not → print the authenticator-app fallback message.\n*45|   Exit codes: success 0; ambiguous/unknown-subnet = usage error; not-elevated = a distinct\n 46|   documented non-zero (the fetch was refused, not failed).\n 47|4. **Activate REQ-PAIR-3, REQ-PAIR-6** in `traceable-reqs.toml` (`[impl,unit]`; the live\n 48|   OS-gate end-to-end is hard to unit-test hermetically — cover the decision/seam in unit,\n*49|   note real-elevation verification is manual via todlando / D9).\n 50|\n 51|## Tests\n*52|- elevation: the gate-decision seam (elevated bool → show vs fallback) both ways; Unknown →\n 53|  fallback; the fallback message names the authenticator app.\n 54|- cli parse: `show-totp`, `--subnet x`, `--create-new`, mutually-exclusive flag rejection.\n*55|- handler (inject SubnetStore + now + a faked elevation): single-subnet auto-select; multi\n 56|  refuses without `--subnet`; `--create-new` mints+shows; code matches `seed.code_at(now)`;\n*57|  not-elevated path shows fallback + no code leak.\n 58|\n 59|## NOT in D2g\n*60|- Elevated **agent endpoint** path (an elevated agent surfacing the code over IPC) → needs\n*61|  the daemon/endpoint ACL (D4/D5); D2g does the node-bound OS-privilege path only.\n 62|- QR image rendering → GUI/later.\n 63|- REQ-PAIR-7 subnet icon → GUI (D9).\n 64|\n\n# M4-PLAN.md\n 117|  `H(subnet-name ‖ TOTP-epoch)`; create-new names the subnet up front. · **REQ-PAIR-5, REQ-PAIR-4**\n*118|- **D2g** Elevation-gated per-subnet code fetch (Win UAC / Linux root or elevated agent\n 119|  endpoint; else authenticator app). `spt pair show-totp [--subnet|--create-new]`. · **REQ-PAIR-6, REQ-PAIR-3**\n 120|- **Reqs:** REQ-PAIR-1..6. · **New hazard reqs to register first (rule 3):** add\n 121|  `REQ-HAZARD-PAIR-TRANSCRIPT-BIND` (#12), `REQ-HAZARD-PAIR-SEED-ROTATION` (#10),\n\n# M7-PLAN.md\n 4|> hardware: fresh two-machine setup via `subnet create` + the user's own\n*5|> fully-guided `subnet join` (no test harness, elevation prompts honored),\n 6|> `subnet status --nodes` labeled+online on both machines, cross-machine\n 7|> `SENT(WAN)` proven in both directions (HFENDULEAM ↔ gravity-linux), and the\n 8|> rig's product-surface join rung carries the pairing `int` evidence. The\n 9|> acceptance run surfaced two publish-blocking field bugs, both fixed before\n*10|> closeout: elevated daemon spawn (KNOWN-HAZARDS 5.7,\n*11|> REQ-HAZARD-ELEVATED-DAEMON-SPAWN — de-elevation seam) and immortal ghost\n 12|> registry rows (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS —\n 13|> silent-peer eviction), plus the AMBIGUOUS-render copy-paste-targets fix.\n*14|> Session harvest (Linux elevation model, NTP-first clock, firewall\n 15|> registration, post-join address seeding, trust-prune verb, M8\n 16|> nounification spec) is logged in `docs/DEFERRED.md`.\n 17|\n...\n 52|| 8 | **Responder always-on** in every member daemon (pre-trust pairing ALPN), per ADR-0005's one-sided-UX intent; the ADR red-team #11 subnet-global rate limiter is the standing-listener guard. No arming step. |\n*53|| 9 | **Elevation gates** (existing `EXIT_NOT_ELEVATED=3` convention): `subnet create` (seed reveal) and `subnet join` (trust-boundary mutation — an unprivileged process must not enroll the machine into an attacker's subnet) and `show-totp` (already gated). `status` ungated (read-only, no secrets). → ADR-0005 amendment, CONTEXT.md updated. |\n 54|| 10 | Guided join flow (joiner side, interactive when args missing): prompt name → prompt code → LAN+relay rendezvous search → ceremony → \"Joined \\\"home\\\" — 2 nodes: …\". Wrong code → clean re-prompt (fresh window), bounded attempts, rate-limit messages surfaced honestly. No-seed-holder-found → actionable hints (member online? same name? clock sync?). First-join home adoption (REQ-INST-15) fires on join as on create. |\n 55|| 11 | `subnet create` output: created-confirmation, current code, `otpauth://` URI, **terminal QR render** (unicode blocks; small `qrcode`-class crate), \"run `spt subnet join <name>` on the other machine\" hint. |\n 56|| 12 | **`spt how-to <topic>`**: in-binary, task-oriented agent instructions (anti-drift: single source = binary; docs-site never duplicates the content). Visible command, **Agent commands** group. Topics v1: `ready` (background-task guidance, `--once` fallback loop, reply mechanics), `send` (send/ring, reply-to, SENT vs QUEUED); bare `how-to` lists topics. Quickstart prompts point agents at it. |\n*57|| 13 | Quickstart rewrite: install (human) → **optional collapsed `<details>` pairing section** (human, elevated, create/join/status + cross-machine payoff line \"same prompts now work across machines\"; no captured two-machine example) → agent **prompt blocks** (copy-paste into agent sessions; agents run `spt how-to ready` / `how-to send` and follow) → offline-delivery demo → \"what just happened\". |\n 58|| 14 | Names: alice→**lea** (sender), bob→**sergey** (receiver). Scope: docs-site (Pages) + releases-repo README + `quickstart_e2e.rs`. Internal docs (CONTEXT.md, ADRs, plans — `ling`/`doyle`) unchanged. |\n 59|| 15 | README (releases-repo): quickstart bullet corrected to `ready`-based flow + pairing mention; status table \"send / ring / ready / poll\" loses `poll`. |\n*60|| 16 | New requirement family minted **before work starts** (TRACEABILITY rule 3): `REQ-SUBNET-*` (namespace surface · guided join e2e · node labels/addressable · elevation-gated membership), REQ-MSG-2 amended (poll merge), REQ-DOCS-x for `how-to`, REQ-PAIR-4/6 notes updated. Exact ids assigned at execution. |\n 61|\n 62|## Deliverables\n 63|\n...\n 78|one-ceremony-per-subnet riding it), CLI initiator (guided + non-interactive),\n*79|elevation gates on `create`/`join`, seed transfer + trust pinning + home\n 80|adoption on join, failure-mode UX. ADR-0005 amendment recorded.\n 81|\n 82|**D4 — docs**:\n...\n 99|1. Fresh two-machine setup completes via `spt subnet create` + `spt subnet join`\n*100|   only — no test harness, elevation prompts honored, `subnet status --nodes`\n 101|   shows both nodes labeled and online on both machines.\n 102|2. Quickstart runs as written: a human pastes the prompt blocks into two agent\n 103|   sessions; messages flow; the collapsed pairing section upgrades the same\n\n# M8-ACCEPTANCE-STEPS.md\n 14|Shorthands below:\n*15|- HF (run in an **elevated PowerShell** on HFENDULEAM):\n 16|  ```powershell\n 17|  $env:SPT_HOME=\"C:\\Users\\decid\\spt-accept\\home\"\n 18|  $spt=\"C:\\Users\\decid\\Documents\\projects\\spt-core\\target\\release\\spt.exe\"\n...\n 25|\n*26|> Legend: **[KIT-SSH]** I can run it. **[HF-elev]** you run, elevated.\n 27|> **[reboot]** disruptive to the CI runner — your call on timing.\n 28|\n 29|---\n...\n 37||---|---|---|\n*38|| 1/3 | election + de-elevation (both OS) | ✅ (Win SPT_HOME-respawn bug found+fixed `f99dc25`) |\n 39|| 2 | Windows install | ✅ |\n 40|| 4 | old verbs gone / hot path | ✅ |\n 41|| 5 | attach/detach + prune | prune ✅ · **detach-flip DEFERRED** (subnet-scoped probe) |\n...\n 47|\n*48|**Fixes shipped this run** (dev-freeform): SPT_HOME survives Win de-elevation\n 49|respawn (`f99dc25`), clean `--nodes` labels (`b24dd61`), bounded liveness\n 50|probe + notice (`acbfbf4`).\n 51|**Deferred** (DEFERRED.md): subnet-scoped liveness probe (detach false-online),\n...\n 74|**Acceptance found a real REQ-INSTALL-6 bug; fixed and re-verified on hardware.**\n*75|- **De-elevation (criterion 3, KH-5.7): PASS** — `sudo spt` lands daemon+state\n 76|  under the elected user, never root (`strace` confirms `setgid`/`setuid`).\n 77|- **Election (criterion 1, decision 8): PASS after fix.** Root cause:\n*78|  `spt/src/main.rs` de-elevated early via `sudo_invoker()` (drop-only) for\n 79|  *every* command, before clap — shadowing BOTH daemon-side election guards\n 80|  (`Daemon::run` + `spawn_detached`), which then always saw `euid != 0` and\n 81|  no-op'd. So `/etc/spt-core/default-user` was never written by any path\n...\n 89|\n*90|## ✅ Criterion 1/3 (2026-06-07, HFENDULEAM) — Windows de-elevation bug found + FIXED + verified\n 91|\n*92|**Windows leg found a second REQ-HAZARD-ELEVATED-DAEMON-SPAWN bug; fixed and re-verified on hardware.**\n*93|- **Respawn (criterion 3): PASS** — elevated `daemon run` respawns de-elevated.\n 94|- **`daemon stop` ANOMALY → FIXED.** First run: `daemon stop` printed\n*95|  `DAEMON_NOT_RUNNING` while `Get-Process` showed two live unelevated `spt`.\n*96|  Root cause: the de-elevation respawn (`deelevate::create_with_token`)\n 97|  rebuilds the child env from the desktop-user token via\n 98|  `CreateEnvironmentBlock` (correct, to fix `%LOCALAPPDATA%`) but **dropped the\n 99|  invoker's `SPT_HOME`** — so the respawned daemon bound the seed-control\n...\n 102|  served the wrong home. `SPT_HOME` is the documented universe-relocation knob\n*103|  (`perch.rs`), so it must survive de-elevation. **Fix:** `apply_env_overrides`\n 104|  overlays `SPT_HOME` onto the rebuilt block (production = no `SPT_HOME` →\n 105|  unchanged). Verified: `daemon run` → respawn under accept-home → `daemon\n 106|  stop` → `DAEMON_STOPPED`, processes gone. Unit: `env_overlay_keeps_explicit_spt_home_alive`.\n...\n 111|Driven over SSH against `~/spt-accept-home`:\n*112|- **De-elevation (criterion 3, KH-5.7 safety): PASS.** Every clean\n 113|  `sudo … spt daemon run` lands the daemon + state under **reavus**\n 114|  (`identity/node.key` owned `reavus:reavus`), **never root**;\n 115|  `/root/.spt-core` is never created. Verified across file/pipe/tty captures.\n...\n 117|  `/etc/spt-core/default-user` is never written and the daemon prints no\n*118|  `DEELEVATED`/`ELECTED` line, in *any* capture mode (incl. forced tty), even\n*119|  though de-elevation provably happens and env (`SUDO_USER`/`SUDO_UID`) +\n 120|  `/etc` writability are confirmed fine. **Not correctness-blocking:**\n 121|  `daemon_target_user` falls back to the sudo invoker each run, so the daemon\n 122|  always lands under the right account whether or not the record persists —\n...\n 127|  - **strace evidence (kitsubito, deploy binary):** `sudo … spt daemon run`\n*128|    runs `setgid(1000)`+`setuid(1000)` (de-elevation confirmed at the syscall\n 129|    level) but performs **no `openat` of `/etc/spt-core/default-user`**\n 130|    (`write_election` never reached) and **no `write(2,…)` of\n*131|    `DEELEVATED`/`ELECTED`** — even though `SUDO_USER=reavus` is present in the\n 132|    env and `getent passwd reavus`→`1000` resolves (so neither the\n 133|    `default_name.is_empty()` nor the `user_by_name()==None` branch should be\n 134|    taken). The confirmed-on-a-real-terminal \"no output + no record\" rules out\n*135|    SSH suppression. **Next step: instrument `deelevate::daemon_target_user`\n 136|    branch-by-branch (eprintln per arm), rebuild, run under sudo** — the\n 137|    binary's actual branch is not what the source predicts; remote tracing\n*138|    can't see the daemon's own stdout. Non-blocking: de-elevation lands state\n 139|    under the invoker every run regardless of the record.\n 140|\n*141|## Criterion 1 + 3 — fresh-install election + de-elevation\n 142|\n\n# M8-ACCEPTANCE.md\n 11|\n*12|Driven over SSH (`reavus@kitsubito` w/ sudo, `decid@enlyzeam` elevated). D4\n 13|built on each from source; throwaway subnets cleaned up after.\n 14|\n 15|- **Criterion 2 — fresh Windows install (enlyzeam).** ✅ `install.ps1`\n...\n 36|- **Criterion 1 — Linux install legs (kitsubito).** ◐ partial. The installer\n*37|  legs all proved: unelevated install → ~/.local/bin + PATH + systemd **user\n 38|  service started** + **graceful printed sudo hints** for the symlink/linger\n 39|  (REQ-INSTALL-6/8); the `sudo ln` symlink made `sudo spt` resolve;\n 40|  `loginctl enable-linger` set. NOT a clean criterion-1: kitsubito is a *live\n 41|  SPT_DEV node*, so the fresh-account election + reboot-reachable legs want a\n*42|  clean user/box (election ladder + de-elevation are unit-covered;\n 43|  `/root` confirmed never contaminated). Bonus: `subnet leave` ran\n*44|  elevation-gated on both nodes (criterion 5's leave verb).\n 45|\n 46|### Bug found by acceptance (FIXED) — notif_id UNIQUE collision\n 47|\n...\n 107|\n*108|These require fresh installs / reboots / elevation prompts / a second or\n 109|third machine / enlyzeam's skewed clock — not performable from this bench.\n 110|Deploy a **D4 binary** to each rig node first (any new binary also self-heals\n 111|hfenduleam's v0.1.0-dead pump — D4 makes that class supervised + visible).\n...\n 116|   (never root); node is reachable **after a reboot** before any manual `spt`\n*117|   (systemd user unit + linger). → criteria 1, 3 (Linux de-elevation live).\n 118|\n*119|2. **Fresh Windows install** (elevated leg): inbound works with zero\n 120|   accumulated dev firewall rules — a join succeeds where M7's\n*121|   NO_SEED_HOLDER failed. Then the **unelevated-install** path: `spt subnet\n 122|   status` renders the blocked-inbound state honestly. → criteria 2, 3.\n 123|\n 124|5. **attach/detach + prune** on a live pair: `spt subnet detach <n>` makes a\n\n# M8-PLAN.md\n 4|> 2026-06-06 M7-closeout grills (nounification shape, subnet attachment verbs,\n*5|> Linux elevation model); the scope-inclusion decisions (former §Open\n 6|> decisions A–G, plus H/I from the 2026-06-07 pump diagnosis) were ratified\n 7|> in the 2026-06-07 grill — see decisions 16–24. Requirements minted before\n 8|> work begins (TRACEABILITY rule 3).\n...\n 12|scoping starts after M8), and make the *installed* binary a first-class\n*13|citizen on both OSes: elevation that works on Linux, inbound that works on\n 14|Windows, a daemon that survives reboot. M8 is the last cheap moment for\n 15|surface breaks; after this, spt-claude-code freezes the verbs it consumes.\n 16|\n...\n 27|3. **The installer's \"next touch\" is already owed** three legs at once\n*28|   (Linux elevation, firewall registration, OS-service) — one coherent\n*29|   install/elevation deliverable instead of three drive-bys.\n 30|\n 31|## Root causes this milestone fixes\n 32|\n...\n 35|   operations have no noun home; `spt daemon` is a hidden verb.\n*36|2. **A user-scope install cannot be elevated on Linux.** The binary lives in\n 37|   `~/.local/bin` (not on sudo's `secure_path`), and `sudo spt` — when it\n 38|   resolves at all — runs the daemon in root's HOME universe. (CLI guidance\n 39|   band-aid shipped post-M7 @ `1359649`; the *model* — symlink + default\n...\n 71|| 5 | **`spt daemon stop\\|status`** noun; hidden `daemon` verb → `daemon run`. Agent-endpoint `shutdown` keeps its name (under `endpoint`). |\n*72|| 6 | **Subnet attachment verbs** (user spec 2026-06-06; CONTEXT §subnet attachment): `spt subnet detach <NAME> [--save]` / `attach <NAME> [--save]` — daemon keeps running, stops/starts advertising + connecting for that subnet; `--save` persists the startup default (renamed from `--auto`, user 2026-06-07 — clearer semantics). `spt subnet leave <NAME>` elevation-gated. |\n*73|| 7 | **Trust-row prune**: `spt subnet prune <node>` (or ratified final shape) — removes a dead identity's TRUST rows; elevation-gated (trust mutation = security surface). BIGNET's two 09ef831e rows are the live test case. |\n*74|| 8 | **Linux elevation model** (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch runs the daemon (and state) under that account, never root. Windows has no equivalent problem (same user profile). |\n 75|| 9 | No deprecation shims, same as M7 decision 1 — zero external CLI consumers until spt-claude-code exists. Breaking renames land clean. |\n*76|| 10 | New requirements minted **before work starts** (TRACEABILITY rule 3): `REQ-CLI-*` family (endpoint noun · daemon noun · hot-path preservation), `REQ-SUBNET-*` additions (attach/detach serve-state · prune · re-pair overwrite · status render), `REQ-INSTALL-*` additions (Linux elevation install leg · firewall registration · OS-service), polish reqs per scope ratification. Exact ids assigned at execution; inactive until their deliverable starts (rule 5). |\n 77|| 11 | **`subnet status` is daemon-aware** (user 2026-06-07): zero-subnet text becomes \"No subnets registered — this node is standalone.\" followed by a daemon-status-dependent blurb (daemon running vs not). Never implies messaging works while the daemon is down. |\n 78|| 12 | **Hint footer only on bare `spt subnet`** (user 2026-06-07): `spt subnet status` drops its \"hint:\" lines; the flagless `spt subnet` view keeps them (amends M7 decision 5's \"always printed\"). |\n*79|| 13 | **Per-machine re-pair trust overwrite** (user 2026-06-07): registry rows carry a stable per-machine identifier (NOT the MAC — it changes between wifi/ethernet; source per open decision G). A **successful pairing ceremony** presenting the same node label AND the same machine identifier as an existing trusted row is treated as a re-pair of that machine: the old identity's trust + registry rows are deleted automatically. Overwrite fires only inside a completed ceremony (TOTP + SPAKE2 + the join-sid...\n 80|| 14 | **Fast convergence covers daemon bringup too** (user 2026-06-07): D4's address seeding targets both cold starts — post-join (seed from the ceremony connection) and post-restart (persist + reload the pump's last-known peer addresses), so a restarted daemon reconnects in seconds, not 60s+. |\n 81|| 15 | **Fast-track advertisement on node state change** (user 2026-06-07): an endpoint coming online / going offline (ready-listener start/stop, rest-state transition, perch death) triggers an immediate `advertise_local` + peer push instead of waiting out the pump cadence. The cadence stays the steady-state floor; the event path is the fast lane. Field motivation: a re-listened endpoint stayed rendered `[0/1]` for up to a full cadence after coming online (2026-06-07 diagnosis tail). |\n 82|\n...\n 87|| 16 | A | **`spt subnet disband` DEFERRED.** `leave` covers the field need; adding `disband` later is non-breaking, so the adapter window doesn't force it. |\n*88|| 17 | B | **OS-service shape: Linux systemd *user* service + `loginctl enable-linger`** (linger enable rides the elevated install leg; daemon starts at boot pre-login, user-universe per KH 5.7, managed via `systemctl --user`). **Windows: scheduled task at-logon** (interactive session, no stored credentials, no session-0 isolation breaking terminal hosting). At-startup S4U variant = later opt-in if a headless Windows node ever exists. |\n 89|| 18 | C | **NTP IN SCOPE — trigger fired**: enlyzeam's system clock is known >1 min off (exceeds the ±1 TOTP window). Shape (C2): **TOTP offset-correction only** — query NTP at ceremony time (both sides), apply the offset to the TOTP calculation in-process; **system-clock fallback** when NTP unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop. Epoch/lease ordering stays monotonic-clock — untouche...\n 90|| 19 | D | **`xtask debug-converge` IN SCOPE** (builds in D4, activates REQ-UPD-6 int, `docs/DEBUG-CONVERGE-PLAN.md` is the spec). The 2026-06-07 pump diagnosis was the hand-rolled version of this table — the tool pays for itself; acceptance 6 gains instrumented evidence. |\n 91|| 20 | E | **Address seeding = narrow patch, concretized**: there is NO existing addr cache (production resolution is the id-only `PeerResolver`, `peerloop.rs`). New minimal `peer-addrs.json` in the identity dir (sibling of `pump-ops.json`): pubkey-hex → last-known iroh NodeAddr. Resolver consults it FIRST, falls back to id-only discovery on miss **or dial failure** (a stale addr never strands a peer). Two writers: pairing ceremony (both sides, from the live connection) + the pump on successful connect...\n...\n 111|pump + responder; persisted `--auto` startup flags in daemon config), the\n*112|all-attached banner's per-subnet states, `subnet leave` (elevation),\n*113|`subnet prune <node>` (elevation; kills the dead-identity dials; BIGNET's\n 114|09ef831e rows are the acceptance fixture). **Re-pair trust overwrite**\n 115|(decisions 13/22): machine identifier = hashed OS machine id (minted-UUID\n 116|fallback) riding the registry rows (additive serde-default, old rows parse\n...\n 122|\n*123|**D3 — install & elevation hardening (the installer's owed touch):**\n 124|Linux: install.sh symlink leg into `/usr/local/bin` (graceful when\n*125|unelevated: print the one-liner instead) + first-`sudo spt` default-account\n 126|election + KH 5.7 interplay verified (daemon + state under the elected\n*127|account, never root). Windows: elevated install leg registers the firewall\n 128|rule (`New-NetFirewallRule`, inbound UDP) **and** the daemon self-detects\n 129|blocked inbound → rendered as the \"no connection\" state in `subnet status` +\n*130|coming-online banner (covers user-scope installs that skip the elevated leg).\n 131|Both OSes: OS-service registration per decision 17 — Linux systemd user\n*132|service + `enable-linger` (linger rides the elevated install leg); Windows\n 133|scheduled task at-logon (REQ-INSTALL-1 third leg — reboot-survivable\n 134|daemon). Installer-script E2E rungs extended (`SPT_INSTALL_E2E` stays\n 135|hermetic — service/firewall legs env-gated off in CI, proven in acceptance\n...\n 171|   survives a reboot and is reachable before any manual `spt` invocation.\n*172|2. **Fresh Windows install** (elevated leg): inbound works with zero\n 173|   accumulated dev rules — a join succeeds where M7's NO_SEED_HOLDER failed;\n*174|   the unelevated-install path renders the blocked-inbound state honestly in\n 175|   `subnet status`.\n\n# MESH-D3-PLAN.md\n 33|- `tombstone(subnet, pubkey, stamp)` — add a tombstone (idempotent). **Dominates** the entry: `is_member`/`members_in` exclude it; **suppresses reinsert**: a later `merge_entry` of that pubkey re-records facts but `is_member` stays false while the tombstone stands.\n*34|- `clear_tombstone(subnet, pubkey) -> bool` — **clear-on-repair**: remove the tombstone (the deliberate elevated re-pair act); the entry, if present, becomes a member again.\n 35|- `is_tombstoned(subnet, pubkey) -> bool` · `is_member(subnet, pubkey) -> bool` (= entry present ∧ ¬tombstoned — **the admission predicate D5's gate will read**) · `members_in(subnet) -> impl Iterator<&RosterEntry>` (visible members only) · `roster_for(subnet) -> (Vec<RosterEntry>, Vec<Tombstone>)` (the slice handed at pairing — members + tombstones for one subnet).\n 36|\n*37|**Tombstone-convergence boundary (document in the module doc):** the tombstone is a grow-set element (propagates, dominates). `clear_tombstone` is a *local* elevated op — cross-node convergence of an *un*-tombstone is **not** D3's job: the compromised path re-pairs under a **fresh never-tombstoned key** (decision 10), so the old tombstone correctly stands; benign N-1 re-seed + timeboxed rotation propagation is **D7**. D3 tests clear-on-repair locally only.\n 38|\n 39|## Part B — `Frame::Seed` grows the roster (additive wire, `spt-net/pairing/wire.rs`)\n 40|\n\n# MESH-D7-PLAN.md\n 24|\n*25|List form (revoke several at once), **elevation-gated** (reuse `trust_mutation_refusal` — the leave/prune gate), **revoke-only** (own identity refuses; use `leave` to exit). For each resolved node (reuse `prune_candidates` resolution — hex/prefix/label):\n 26|\n 27|- **Tombstone now** in every subnet the node is a member of (`roster.tombstone`, the propagating grow-set marker — unlike prune's local-only tombstone). The tombstone gossips, so other members drop the revokee from their `is_member` gate within a roster round.\n 28|- **Schedule one rotation** at the close of a coalescing window (default 1h). Write/extend a pending-rotation marker (`identity/rotation-pending.json`: `{subnet → {deadline_ms, revokees:[hex]}}`). A second revoke inside the window joins the SAME entry (union the revokees, keep the earliest deadline) ⇒ one epoch bump.\n...\n 67|\n*68|- `spt subnet revoke <node>...` tombstones (propagating) + schedules one coalescing rotation; `--force-rotate-seed` rotates now; elevation-gated; own-identity refuses.\n 69|- A fired rotation bumps `seed_epoch`, force-drops revokees, and benign N-1 on-roster members auto-heal via a reseed_only connection that carries ONLY the new seed; N-2/off-roster/tombstoned are denied (→ re-pair).\n 70|- The seed never appears in roster/registry gossip; the per-conn proof grades exact/prior/stale (the deferred D2 epoch cross-check closed).\n\n[Showing lines 1-589 of 592 (50.0KB limit). Use :590 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":".","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":162,"fileCount":20,"files":["CHANGELOG.md","CI-SELFHOST-PLAN.md","CONTEXT.md","DAEMON-LIFECYCLE-PLAN.md","DAEMON-LIFECYCLE-VERIFICATION.md","M12-CANDIDATES.md","M12-PLAN.md","M12-W2.5-RULING.md","M12-W4-PLAN.md","M12-W4-RULING.md","M4-D2-WIRE-PLAN.md","M4-D2f-PLAN.md","M4-D2g-PLAN.md","M4-PLAN.md","M7-PLAN.md","M8-ACCEPTANCE-STEPS.md","M8-ACCEPTANCE.md","M8-PLAN.md","MESH-D3-PLAN.md","MESH-D7-PLAN.md"],"fileMatches":[{"path":"CHANGELOG.md","count":10},{"path":"CI-SELFHOST-PLAN.md","count":1},{"path":"CONTEXT.md","count":6},{"path":"DAEMON-LIFECYCLE-PLAN.md","count":6},{"path":"DAEMON-LIFECYCLE-VERIFICATION.md","count":2},{"path":"M12-CANDIDATES.md","count":6},{"path":"M12-PLAN.md","count":6},{"path":"M12-W2.5-RULING.md","count":1},{"path":"M12-W4-PLAN.md","count":20},{"path":"M12-W4-RULING.md","count":20},{"path":"M4-D2-WIRE-PLAN.md","count":1},{"path":"M4-D2f-PLAN.md","count":1},{"path":"M4-D2g-PLAN.md","count":20},{"path":"M4-PLAN.md","count":1},{"path":"M7-PLAN.md","count":9},{"path":"M8-ACCEPTANCE-STEPS.md","count":20},{"path":"M8-ACCEPTANCE.md","count":8},{"path":"M8-PLAN.md","count":20},{"path":"MESH-D3-PLAN.md","count":2},{"path":"MESH-D7-PLAN.md","count":2}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# CHANGELOG.md\n  348│\n *349│A join-truth release: joining a subnet now tolerates a skewed or stepped system clock so it finds members it used to silently miss, no longer raises the elevated-permission prompt before a member is actually found, and can show the exact ceremony clock it is using — plus a loud warning when no time server can be reached.\n  350│\n  351│### Added\n  352│\n     │...\n  357│\n *358│- **Joining no longer raises the elevated-permission prompt before a member is found.** `spt subnet join` now asks for OS elevation only once it has actually located a subnet member, rather than up front — so a join that can't find anyone no longer pops an elevation prompt for nothing. The `--code` path is unchanged.\n  359│\n  360│### Fixed\n  361│\n     │...\n  919│- **`spt subnet join` shows a QR code + setup code on success.** After joining, scan the QR (or read the `otpauth://` code) to re-provision an authenticator app for the subnet.\n *920│- **Privilege-gated commands self-elevate, cross-platform.** When a command needs elevation, spt re-launches itself the right way for your system — a Windows UAC prompt, a Linux desktop `pkexec`/terminal `sudo`, or inline `sudo` in a terminal — and prints the exact command to run by hand when it can't.\n  921│- `spt spt` — ???\n  922│\n  923│### Changed\n     │...\n 1001│\n*1002│- **`spt subnet revoke <node>…`** — remove one or more machines from a subnet across the **whole fleet**, not just locally. It tells every member to drop the node within moments, then rotates the subnet's shared secret so the removed machine is locked out and must re-pair to come back. By default the rotation is batched at the end of a one-hour window — several revokes in that window share a single rotation, and any member that was briefly offline heals automatically across it. Pass `--force-rotate...\n 1003│- **`spt daemon start`** — bring the daemon up in the background, idempotently. When `spt` is installed as a service (the Linux per-user service, or the Windows logon task), `start` and `stop` now drive *that* service instead of a stray hand-started daemon — so the two never fight each other for the connection. `spt daemon start` on an already-running daemon just says so and does nothing.\n 1004│\n 1005│### Changed\n     │...\n 1009│- **`spt daemon stop` is service-aware.** If a managed service owns the daemon, `stop` asks the service manager to stop it cleanly (so it doesn't immediately restart), instead of signalling the process directly. A hand-started daemon still stops the same way as before.\n*1010│- **`spt daemon run` is now strictly foreground on every platform** — it stays attached to your terminal until you stop it (the form the installed service uses). For a background daemon, use `spt daemon start`. On Windows, running `daemon run` from an elevated shell now refuses with a hint rather than silently disappearing into the background.\n 1011│- **`spt daemon status` shows what manages the daemon** — whether a service owns it (and is active) or it was started by hand.\n 1012│\n 1013│### Performance\n     │...\n 1044│- **Automatic re-pair cleanup.** If you reinstall or regenerate a node's identity and pair it again from the same machine under the same name, its old, now-dead identity is removed automatically during pairing — no manual prune needed.\n*1045│- **Firewall setup on Windows.** The installer (when run elevated) now adds the inbound network rule `spt` needs so other nodes can reach you. If it wasn't added, `spt subnet status` and the \"coming online\" banner now tell you the rule is missing and print the exact command to add it.\n 1046│- **Starts on boot.** The installer now registers `spt` to start automatically — at login on Windows, and as a per-user service on Linux — so your node is reachable after a reboot without running a command first.\n 1047│\n 1048│### Changed\n     │...\n 1066│- **Messages from other agents now arrive properly formatted.** Incoming messages on the listener stream now include the full envelope with the sender's name, instead of showing as a raw, unwrapped line.\n*1067│- **`sudo spt …` now works on Linux user installs.** When `spt` is installed to your user directory, elevation guidance that said to \"run as administrator/root\" used to dead-end with `sudo: spt: command not found`. The installer now also makes `spt` reachable under `sudo`, and on an interactive terminal `spt` re-runs itself with `sudo` automatically; otherwise it prints a command that actually works.\n*1068│- **Elevated `spt` on Linux runs under your account, not root.** The first time you run an elevated `spt`, it asks once which account should own the daemon and its data, remembers that choice, and every later `sudo spt` runs the daemon and stores state under that account — never as root.\n*1069│- **`spt daemon stop` on Windows now finds the daemon it started.** A daemon launched through Windows' elevation prompt could end up using the wrong home directory, so `spt daemon stop` reported \"daemon not running\" while a daemon kept running. It now keeps the right home directory across that elevation step.\n*1070│- **Removed a confusing internal status line.** `spt` no longer prints the internal \"DEELEVATED: running as uid …\" notice during normal use.\n 1071│- **Stale node rows clear out on their own.** Nodes that haven't been heard from in a while are now removed from the listing automatically, so old/dead entries stop cluttering `spt subnet status`.\n 1072│\n 1073│## [0.1.1] - 2026-06-07\n\n# CI-SELFHOST-PLAN.md\n 21│\n*22│1. **Registration tokens** (repo admin):\n 23│   `gh api -X POST repos/SaberMage/spt-core/actions/runners/registration-token --jq .token`\n 24│2. **HFENDULEAM runner**: download actions-runner (win-x64), `config.cmd --url\n 25│   https://github.com/SaberMage/spt-core --token <T> --labels hfenduleam\n\n# CONTEXT.md\n 727│**seed rotation / revoke**:\n*728│Removing a member is **`spt subnet revoke <node>...`** (elevation-gated, revoke-only — *adding* a member never rotates; the joiner just receives the current seed at pairing). Two effects:\n 729│- **Immediate:** writes a **roster tombstone** per revoked pubkey (see *member roster*) — propagates over member connections, suppresses that pubkey under the roster's union-merge, and augments the inbound gate to **membership-proof ∧ ¬tombstoned** so the node can't reconnect-and-reinsert (it still holds the seed until rotation). Force-drops its connections.\n 730│- **Coalesced rotation:** the tombstone schedules **one** seed rotation (re-mint seed, bump the **seed-rotation epoch** — `SubnetRecord.epoch`, ADR-0005 #10; push the new seed confidentially over member-authenticated TLS connections, **never** in roster/registry gossip — the seed is not roster data) at the close of a **coalescing window (default 1 h)**. Further revokes within the window join the same rotation → **one epoch bump** however many nodes, keeping benign offliners inside the single-epoch ...\n 731│A completed re-pair ceremony for a tombstoned pubkey clears its tombstone (deliberate re-admit).\n    │...\n 766│\n*767│**Node-bound code fetch — and every subnet-membership mutation — is gated behind OS privilege elevation** (Windows UAC / Linux root-or-equivalent): retrieving a subnet's code *from the node*, minting a new subnet (`subnet create` — a seed reveal), and joining one (`subnet join` — enrolling the machine into a trust fabric) all require either hardware/elevated access OR an **elevated endpoint** (an agent whose process is elevated can surface it). The join gate exists because membership is a trust-b...\n 768│\n*769│**self-elevating re-launch (cross-platform)**:\n*770│<!-- [doc->REQ-ELEVATE-1] -->\n*771│When a gated command is run unelevated, spt does not just print \"run as administrator\" — it **re-launches itself with privilege** so the user reaches the result in one step. The path is chosen by a pure decision seam (`elevation::decide_elevation_path`) from the OS, the current elevation, and the environment: an **interactive Unix TTY** re-execs inline under `sudo`; a **Linux desktop without a TTY** prefers **`pkexec`** (native polkit GUI auth, clean stdio) and falls back to a **terminal-emulator** (`x...\n 772│\n 773│**node label**:\n 774│A human display name for a node, defaulting to the machine's OS hostname (re-checked at daemon startup; a hostname change updates the label). Advertised through the existing registry gossip so subnet views render `HFENDULEAM (bcead52b…)` instead of bare key hex. The pubkey remains the identity; the label is **addressable**: an `@node` qualifier accepts a label or a key-prefix (`ling@hfenduleam`). Labels are not unique — an ambiguous label follows the resolution policy's refuse-and-qualify rule (refus...\n    │...\n 832│**arriving-message envelope**:\n*833│Every message that arrives at a consumer — an agent over `api listen`/`spt ready`, an adapter's hook composer over `api poll`/`api worker-poll`, the cross-node WAN feed — is delivered as the canonical **`<EVENT type=\"…\" from=\"…\">body</EVENT>` envelope** (`spt-proto::event`, the ADR-0001 grammar; self-delimiting, `from` attribution, `<br>` body escaping, `<EVENT-PART>` chunking). It is the one format adapters parse on, identical across every **arriving-message** surface on an agent perch. The earl...\n 834│\n 835│**file-transfer progress**:\n 836│Every file transfer over the substrate is addressable and **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI) — at any point during the transfer. A substrate-wide requirement, not shell-specific (the Shell text+file channel inherits it).\n\n# DAEMON-LIFECYCLE-PLAN.md\n  29│- `crates/spt-daemon/src/daemon.rs`: `ensure_running()` → `spawn_detached()`\n *30│  (Linux fork-style; Windows `DETACHED_PROCESS | CREATE_NO_WINDOW`, de-elevated\n *31│  via `deelevate`). `request_stop()` = IPC stop.\n  32│- Installer-registered services:\n  33│  - Linux (`install.sh`): systemd **user** unit `~/.config/systemd/user/spt-daemon.service`,\n  34│    `ExecStart=<bin> daemon run`, `enabled`; `loginctl enable-linger` for pre-login boot.\n  35│  - Windows (`install.ps1`): at-logon **scheduled task** `spt-core daemon`\n *36│    (`schtasks /SC ONLOGON /TR \"<bin> daemon run\"`) — needs elevation to register.\n  37│\n  38│## Target model (consistent across platforms)\n  39│\n    │...\n  68│  - (a) **Promote to a real Windows Service** (`sc.exe create` / a small service\n *69│    host) → clean `start`/`stop` parity with systemd. Heavier; needs elevation\n  70│    + a service-mode entry in the binary.\n  71│  - (b) **Keep the scheduled task for boot-start; `start` = detached spawn,\n  72│    `stop` = IPC stop.** Simpler; honest asymmetry (Windows has no per-user\n    │...\n  84│- Audit why Windows `spt daemon run` goes invisible-background (likely the\n *85│  `Daemon::run()` de-elevation respawn / a `spawn_detached` on the `run` path).\n  86│  Make `run` strictly foreground on Windows — the detached/invisible behavior\n  87│  lives ONLY in `start`. Keep `CREATE_NO_WINDOW` for the `start` detached spawn,\n  88│  not for foreground `run`.\n    │...\n 111│- v0.2.0 already registered the systemd unit on kitsubito + the scheduled-task\n*112│  attempt on HFENDULEAM (HFENDULEAM's task needs an elevated re-run — still open).\n 113│- After this ships: re-roll to the fleet so `start`/`stop` drive the managed\n 114│  service everywhere; document the daemon-lifecycle UX in the CLI reference +\n 115│  CHANGELOG.\n\n# DAEMON-LIFECYCLE-VERIFICATION.md\n 49│\n*50│- REQ-DAEMON-7's elevated-`run`-refuses behavior is Windows-only (N/A on this\n 51│  rig); Linux `daemon run` is foreground by construction (the in-process\n*52│  privilege drop keeps it foreground), which the unit's `ExecStart` relies on.\n 53│- The internal `ensure_running` auto-start shares the exact `plan_start(false,\n 54│  detected) → ServiceStart → systemctl --user start` path proven in step 3; a\n 55│  dedicated CLI trigger was skipped to avoid mutating live node state.\n\n# M12-CANDIDATES.md\n 18│\n*19│2. **Subnet create/join QR + self-elevating window.** `spt subnet create/join/show-totp`:\n 20│   - Surfaces a **QR code** of the TOTP seed for the user to store → must spawn a\n 21│     window to display it (create AND join).\n 22│   - Behavior change (per operator sketch): runs normally if env is interactive +\n*23│     elevated; ELSE re-runs self in a self-elevating window with inline stdout\n*24│     (\"Elevated terminal launched. Accept the prompt and proceed there.\" → \"You can\n 25│     close this window.\", no auto-close).\n*26│   - Cross-platform elevation fallbacks: Windows = UAC self-elevate window;\n 27│     Linux+desktop = pkexec/polkit or x-terminal-emulator; Linux+TTY = inline sudo;\n 28│     headless/no-TTY = print exact command for the human (agent relays).\n*29│   - Scope elevation to only steps that need it (service-install / firewall /\n*30│     privileged-port) — subnet-create itself likely unprivileged.\n 31│   - Complementary downstream: spt-claude-code skill with create/join/show-code verbs.\n 32│\n 33│3. **spt-hosted HARNESS bringup + local user PTY attach (CLI). — CONFIRMED MISSING, TOP PRIORITY.**\n\n# M12-PLAN.md\n 56│\n*57│## Wave 4 — subnet UX (QR + self-elevation)\n 58│- **T4.1** `spt subnet create/join/show-totp`: surface a **QR code** of the TOTP seed (window\n 59│  on create AND join).\n*60│- **T4.2** Self-elevating behavior: run normally if interactive+elevated; else re-run self in\n*61│  a self-elevating window with inline stdout (\"Elevated terminal launched… / You can close\n 62│  this window.\"). Cross-platform: Win UAC window · Linux+desktop pkexec/polkit or\n 63│  x-terminal-emulator · Linux+TTY inline sudo · headless/no-TTY print command for the human.\n*64│  Scope elevation to only steps that need it (service/firewall/privileged-port; subnet-create\n*65│  itself likely unprivileged).\n 66│\n 67│## Wave 5 — whoami merge (small)\n 68│- **T5.1** `spt whoami` → alias for `spt endpoint list`; SELF-pin output additionally includes\n   │...\n 73│  cross-node attach proven; file-backed strings + adapter:profile fallback work; subnet QR +\n*74│  elevation on Win+Linux; whoami alias. Full suite green; `traceable-reqs check` EXIT=0;\n 75│  cross-subnet/PTY proof demonstrated (this is the spt-claude-code v1 acceptance dependency).\n 76│- Then unblock perri to begin `spt-claude-code` (see `../spt-claude-code/SCOPE.md`).\n\n# M12-W2.5-RULING.md\n 57│- Gate: `access_check(endpoint, origin, Unsolicited)` already decides who may control; taking\n*58│  control is the same gate (no elevated kick policy — if you may drive, you may take).\n 59│\n 60│### 6. Picker (W2 picker integration)\n 61│blue ■ = a controller is present (`driven_by` = controller node) **+ a viewer count** (\"controlled\n\n# M12-W4-PLAN.md\n *1│# M12 Wave 4 — subnet QR + self-elevating window — JIT plan\n  2│\n  3│> todlando 2026-06-14, after the W2 + follow-on batch gate (PASS, committed\n *4│> 6e76f62 / eb74b18). doyle: \"proceed to W4 subnet QR + self-elevating window;\n  5│> ping a design-check JIT like the others.\" **DESIGN-CHECK PENDING — build only\n *6│> after doyle's ruling.** The load-bearing unknown is T4.2 (self-elevating\n *7│> *window*): cross-platform + a security surface (spawning elevated processes /\n  8│> UAC). Spec: `M12-PLAN.md` Wave 4 + `M12-CANDIDATES.md`.\n  9│\n 10│## Scope (M12-PLAN.md Wave 4)\n   │...\n 12│  seed (on create AND join).\n*13│- **T4.2** **Self-elevating behavior**: run normally if interactive + elevated;\n*14│  else re-run self in a self-elevating window with inline stdout (\"Elevated\n 15│  terminal launched… / You can close this window\"). Cross-platform — Win UAC\n 16│  window · Linux+desktop pkexec/polkit or x-terminal-emulator · Linux+TTY inline\n*17│  sudo · headless/no-TTY print the command for the human. Scope elevation to only\n*18│  the steps that need it (service/firewall/privileged-port; subnet-create itself\n*19│  is unprivileged-to-run but reveals a secret, so it is gated).\n 20│\n 21│## What EXISTS today (investigation 2026-06-14)\n 22│- **QR already renders** (M7/M8, REQ-SUBNET-1): `render_qr` (cli.rs:2964) encodes\n   │...\n 26│  shipped** — the open question is whether anything is actually missing (Q1).\n*27│- **Elevation is PARTIALLY built** (`spt/src/elevation.rs`, REQ-PAIR-6 /\n 28│  REQ-HAZARD-SUDO-SECURE-PATH):\n 29│  - `current()` probe (Win token / Unix euid, fail-safe `Unknown`); `gate_for`\n 30│    (Show/Fallback); `fallback_message`.\n*31│  - `current_style()` (Unix `Sudo` / Win `AdminTerminal`); `rerun_command`\n*32│    (absolute-path sudo line, secure_path-proof); `should_auto_elevate` (Unix ∧\n*33│    interactive ∧ NotElevated).\n*34│  - `cli.rs::try_auto_elevate` ACTUALLY re-execs `sudo <abs-exe> <args>` on an\n*35│    **interactive Unix TTY** (the elevated child does the work; `main` de-elevates\n*36│    back via the 5.7 drop). `with_elevation_hint` prints the absolute-path sudo\n 37│    command otherwise. Consumed by `cmd_subnet_create` + `cmd_subnet_show_code`.\n 38│- **So the W4 T4.2 DELTA is the rest of the matrix:** today only **Unix +\n*39│  interactive-TTY** auto-elevates (inline sudo); **Windows prints \"run as\n*40│  administrator\" with NO actual UAC re-launch**; **Linux desktop without a TTY**\n 41│  (DISPLAY but no terminal) has no path; **headless** falls to the printed hint\n 42│  (Unix only). T4.2 = add the **Windows UAC window**, the **Linux desktop\n 43│  pkexec/terminal-emulator** path, and a **headless print** that covers Windows too.\n*44│- De-elevation seam exists (`spt-daemon::deelevate`, REQ-HAZARD-ELEVATED-DAEMON-SPAWN)\n*45│  — the elevated child drops back to the user for state safety; the\n\n# M12-W4-RULING.md\n *1│# M12 W4 — doyle design ruling (subnet QR + self-elevation)\n  2│\n *3│> doyle 2026-06-14, gating todlando's W4 JIT (M12-W4-PLAN.md). Privilege-escalation feature →\n  4│> security conditions stated explicitly + a mandatory hazard REQ. Primitives verified vs source\n  5│> before ruling.\n  6│\n  7│## Verified against source\n *8│- `elevation::rerun_command` enforces the **absolute exe path** (secure_path-proof, KH 5.10) — test `sudo_rerun_uses_absolute_path_not_bare_name` asserts `starts_with(\"sudo /\")`.\n *9│- `try_auto_elevate` (cli.rs:3065) re-execs `sudo <current_exe_str> <invocation_args>` — **verbatim args**, no widening.\n*10│- `ELEVATION_PROVEN` flag + `current()` (elevation.rs:164) short-circuit to `Elevated` once proven → **loop-guard foundation exists**.\n 11│- `render_qr` (cli.rs:2964) renders the `otpauth://` URI on create / show-code / join. T4.1 largely shipped.\n 12│\n 13│The matrix builds on sound primitives. Rulings:\n   │...\n 15│## Q1 — T4.1 QR: verify + close the join gap; \"window\" = inline terminal QR, NO GUI. ✓\n*16│Confirm `cmd_subnet_join` renders the QR (not only the code) post-join; add the `render_qr` call if missing, else no-op. No GUI milestone exists — reading \"window\" as a GUI surface would invent scope. (The \"window\" in the *milestone title* belongs to T4.2's elevated console, not a QR surface.)\n 17│\n*18│## Q2 — Windows UAC child-window: `runas` on the abs-exe DIRECTLY, child self-pauses, NO stdout marshaling.\n*19│`ShellExecuteW` with the `runas` verb on **`<abs-exe> <verbatim-args>`** — **not** a `cmd /k` wrapper (a `cmd /k` leaves an interactive *privileged shell* open = needless escalated surface). The elevated child detects it is the elevated re-launch, does the work, prints the result + \"You can close this window,\" and **pauses for a keypress** so the user reads it. The original process prints \"Elevated terminal launched…\" and exits 0.\n*20│- **Security (binding): never pipe / capture the elevated child's stdout back across the privilege boundary.** The unprivileged parent must not depend on privileged output. The child is self-contained.\n 21│- Verbatim args + absolute exe path (same secure-path discipline as the sudo leg, applied to the Windows launcher).\n 22│\n 23│## Q3 — Linux desktop order: confirmed; argv-array, never a shell string.\n 24│interactive-TTY → inline `sudo` (existing) → else `DISPLAY` ∧ `pkexec` → `pkexec` → else `DISPLAY` ∧ terminal-emulator → `x-terminal-emulator -e sudo …` (fallback list: x-terminal-emulator → gnome-terminal → konsole → xterm) → else → print the absolute-path sudo hint. pkexec preferred (native polkit GUI auth, clean stdio, no extra window).\n*25│- **Security (binding): pass argv as an ARRAY to pkexec / the terminal-emulator / runas — never a shell-interpolated command string.** A crafted id / path / arg must not be able to inject a second command. (`Command::new(...).args([...])`, not `sh -c \"<interpolated>\"`.)\n 26│\n 27│## Q4 — pure decision seam: APPROVED.\n*28│`decide_elevation_path(os, elevation, interactive_tty, has_display, has_pkexec, has_term_emulator) -> ElevatePath { AlreadyElevated, InlineSudo, UacWindow, Pkexec, TerminalEmulator, PrintHint }`, pure + unit-tested across the matrix; each variant's actual launch is the impure manual-verify leg (REQ-PAIR-6 precedent). Generalizes `should_auto_elevate`.\n*29│- **Required matrix coverage: `AlreadyElevated` returned whenever `current()==Elevated` (or `ELEVATION_PROVEN`), on EVERY os** — this is the loop-safety guarantee expressed in the testable seam. The elevated child re-enters, sees `Elevated`, and never re-elevates.\n 30│\n*31│## Q5 — REQ granularity: NEW REQ-ELEVATE-1. ✓\n*32│Cross-platform self-elevating re-launch is a distinct mechanism reused by every gated command (not subnet-specific). New **REQ-ELEVATE-1** (decision matrix + per-OS window launch + inline-notice UX). doc+impl+unit, **no int** (launch is manual-verify).\n 33│\n 34│## Q6 — Security posture: YES, add a hazard REQ. NON-NEGOTIABLE.\n*35│A privilege-escalation feature MUST carry a `REQ-HAZARD-*` with conformance tests. Add **REQ-HAZARD-SELF-ELEVATE** (required: unit; the hazard = a self-elevation that widens scope, hijacks PATH, injects via a shell string, or loops). The invariant:\n 36│\n*37│> Self-elevation re-runs the EXACT original invocation with the **absolute exe path** — never widening privilege scope, never adding/altering args, never via a PATH-resolved bare name, never via a shell-interpolated string (argv-array only); the elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates** (loop-safe via `AlreadyElevated`).\n 38│\n 39│Unit conformance (extends the existing abs-path test across the new launchers):\n*40│1. Every launcher's command uses the absolute exe path + verbatim args (Win `runas`, pkexec, terminal-emulator, sudo).\n*41│2. `decide_elevation_path` → `AlreadyElevated` when elevated, every os (loop-safety).\n 42│3. Launch argv is an array, not a shell string (no-injection — assert the constructed argv, no `sh -c`).\n 43│\n*44│The existing REQ-HAZARD-SUDO-SECURE-PATH covers the Unix abs-path-under-sudo facet; REQ-HAZARD-SELF-ELEVATE covers the cross-platform verbatim / no-widen / no-shell-injection / loop-safe facets. The \"print hint\" floor must also print the **absolute-path** command (copy-paste stays secure_path-proof).\n 45│\n 46│## Scope confirmation\n 47│W4 is mostly T4.2's Windows + Linux-desktop launch arms over the existing pure-seam pattern — smaller than W2. Agreed.\n   │...\n 49│## Binding gate conditions (I verify all three)\n*50│1. **Loop-safety** proven in the pure seam: `AlreadyElevated` short-circuit on every os.\n\n# M4-D2-WIRE-PLAN.md\n 109│D2f (multi-subnet ceremony inputs: code+name, rendezvous token, create-new names up front —\n*110│REQ-PAIR-4/5) → D2g (elevation-gated `spt pair show-totp` — REQ-PAIR-3/6) → D3 registry.\n\n# M4-D2f-PLAN.md\n 59│  library mechanism (create_subnet + named ceremony + seed transfer) is what D2f delivers.\n*60│- Elevation-gated code fetch (`spt pair show-totp`) → D2g (REQ-PAIR-6, REQ-PAIR-3).\n 61│\n 62│## Conventions (carried)\n 63│- NO `cargo fmt`. Tag `[impl->REQ-PAIR-{4,5}]` on real evidence, `[unit->…]` on tests.\n\n# M4-D2g-PLAN.md\n *1│# M4-D2g — elevation-gated per-subnet code fetch (JIT plan)\n  2│\n  3│**Status:** not started. D2f (multi-subnet + seed transfer) shipped & CI-green at `452358d`;\n  4│REQ-PAIR-4/5 `[impl,unit]`. D2g is the LAST D2 task: the local `spt pair show-totp` command\n *5│that surfaces a subnet's current code, **gated behind OS privilege elevation** (REQ-PAIR-6),\n  6│fetchable per-subnet from any paired node (REQ-PAIR-3). Local-only — no daemon/P2P, so\n  7│buildable now without the D4 net wiring.\n  8│\n   │...\n 11│`spt pair show-totp [--subnet <name> | --create-new]` prints the subnet's current 6-digit\n*12│code + `otpauth://` URI **only when the process is elevated** (Windows UAC / Linux\n 13│root-or-equiv); otherwise it refuses the node-bound fetch and points the user to their\n 14│authenticator app (where the seed was provisioned at pairing). Per-subnet because a node\n 15│holds N seeds (ADR-0006); `--create-new` mints+names a fresh subnet (sole seed-holder).\n   │...\n 18│\n*19│- **Real elevation gate** (todlando is available for admin-enabled testing on this machine).\n*20│  Linux: `libc::geteuid() == 0`. Windows: `GetTokenInformation(.., TokenElevation, ..)` via\n 21│  the `windows` crate. `cfg`-split; an unknown/unsupported platform returns `Unknown` →\n*22│  treated as NOT elevated (fail safe → app fallback). Keep deps leaf-local (the elevation\n 23│  util crate only), preserving the dependency-minimal core.\n*24│- **Where:** put `is_elevated()` / `Elevation` in the `spt` binary (CLI-adjacent, e.g.\n*25│  `crates/spt/src/elevation.rs`) — it gates a CLI command, and only the binary need pull the\n 26│  OS deps. The code fetch itself reads `spt-store::SubnetStore` (already there).\n 27│- **No-flag behavior:** if the node holds exactly one subnet, use it; if several, **refuse +\n 28│  require `--subnet`** (no guessing — same ethos as D3c addressing). Zero subnets → tell the\n   │...\n 34│\n*35│1. **`elevation.rs`** (REQ-PAIR-6) — `enum Elevation { Elevated, NotElevated, Unknown }` +\n*36│   `fn current() -> Elevation`. `#[cfg(unix)]` libc geteuid; `#[cfg(windows)]` windows-crate\n*37│   TokenElevation; else Unknown. Unit-test the mapping logic where possible (the OS call\n 38│   itself is environment-dependent — gate behind a thin testable seam: a function taking the\n*39│   raw elevated-bool and producing the gate decision + message).\n 40│2. **`Cmd::Pair { ShowTotp { subnet, create_new } }`** in `cli.rs` — clap subcommand, stable\n 41│   arg shapes (REQ-MSG-2 style), parse tests.\n 42│3. **Handler** (REQ-PAIR-3) — load SubnetStore; resolve subnet (flag / single / refuse);\n*43│   `--create-new` → `create_subnet`; gate on `Elevation::current()`; elevated → print code\n 44│   (`seed.code_at(now)`) + `otpauth_uri`; not → print the authenticator-app fallback message.\n*45│   Exit codes: success 0; ambiguous/unknown-subnet = usage error; not-elevated = a distinct\n 46│   documented non-zero (the fetch was refused, not failed).\n 47│4. **Activate REQ-PAIR-3, REQ-PAIR-6** in `traceable-reqs.toml` (`[impl,unit]`; the live\n 48│   OS-gate end-to-end is hard to unit-test hermetically — cover the decision/seam in unit,\n*49│   note real-elevation verification is manual via todlando / D9).\n 50│\n 51│## Tests\n*52│- elevation: the gate-decision seam (elevated bool → show vs fallback) both ways; Unknown →\n 53│  fallback; the fallback message names the authenticator app.\n 54│- cli parse: `show-totp`, `--subnet x`, `--create-new`, mutually-exclusive flag rejection.\n*55│- handler (inject SubnetStore + now + a faked elevation): single-subnet auto-select; multi\n 56│  refuses without `--subnet`; `--create-new` mints+shows; code matches `seed.code_at(now)`;\n*57│  not-elevated path shows fallback + no code leak.\n 58│\n 59│## NOT in D2g\n*60│- Elevated **agent endpoint** path (an elevated agent surfacing the code over IPC) → needs\n*61│  the daemon/endpoint ACL (D4/D5); D2g does the node-bound OS-privilege path only.\n 62│- QR image rendering → GUI/later.\n 63│- REQ-PAIR-7 subnet icon → GUI (D9).\n 64│\n\n# M4-PLAN.md\n 117│  `H(subnet-name ‖ TOTP-epoch)`; create-new names the subnet up front. · **REQ-PAIR-5, REQ-PAIR-4**\n*118│- **D2g** Elevation-gated per-subnet code fetch (Win UAC / Linux root or elevated agent\n 119│  endpoint; else authenticator app). `spt pair show-totp [--subnet|--create-new]`. · **REQ-PAIR-6, REQ-PAIR-3**\n 120│- **Reqs:** REQ-PAIR-1..6. · **New hazard reqs to register first (rule 3):** add\n 121│  `REQ-HAZARD-PAIR-TRANSCRIPT-BIND` (#12), `REQ-HAZARD-PAIR-SEED-ROTATION` (#10),\n\n# M7-PLAN.md\n   4│> hardware: fresh two-machine setup via `subnet create` + the user's own\n  *5│> fully-guided `subnet join` (no test harness, elevation prompts honored),\n   6│> `subnet status --nodes` labeled+online on both machines, cross-machine\n   7│> `SENT(WAN)` proven in both directions (HFENDULEAM ↔ gravity-linux), and the\n   8│> rig's product-surface join rung carries the pairing `int` evidence. The\n   9│> acceptance run surfaced two publish-blocking field bugs, both fixed before\n *10│> closeout: elevated daemon spawn (KNOWN-HAZARDS 5.7,\n *11│> REQ-HAZARD-ELEVATED-DAEMON-SPAWN — de-elevation seam) and immortal ghost\n  12│> registry rows (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS —\n  13│> silent-peer eviction), plus the AMBIGUOUS-render copy-paste-targets fix.\n *14│> Session harvest (Linux elevation model, NTP-first clock, firewall\n  15│> registration, post-join address seeding, trust-prune verb, M8\n  16│> nounification spec) is logged in `docs/DEFERRED.md`.\n  17│\n    │...\n  52│| 8 | **Responder always-on** in every member daemon (pre-trust pairing ALPN), per ADR-0005's one-sided-UX intent; the ADR red-team #11 subnet-global rate limiter is the standing-listener guard. No arming step. |\n *53│| 9 | **Elevation gates** (existing `EXIT_NOT_ELEVATED=3` convention): `subnet create` (seed reveal) and `subnet join` (trust-boundary mutation — an unprivileged process must not enroll the machine into an attacker's subnet) and `show-totp` (already gated). `status` ungated (read-only, no secrets). → ADR-0005 amendment, CONTEXT.md updated. |\n  54│| 10 | Guided join flow (joiner side, interactive when args missing): prompt name → prompt code → LAN+relay rendezvous search → ceremony → \"Joined \\\"home\\\" — 2 nodes: …\". Wrong code → clean re-prompt (fresh window), bounded attempts, rate-limit messages surfaced honestly. No-seed-holder-found → actionable hints (member online? same name? clock sync?). First-join home adoption (REQ-INST-15) fires on join as on create. |\n  55│| 11 | `subnet create` output: created-confirmation, current code, `otpauth://` URI, **terminal QR render** (unicode blocks; small `qrcode`-class crate), \"run `spt subnet join <name>` on the other machine\" hint. |\n  56│| 12 | **`spt how-to <topic>`**: in-binary, task-oriented agent instructions (anti-drift: single source = binary; docs-site never duplicates the content). Visible command, **Agent commands** group. Topics v1: `ready` (background-task guidance, `--once` fallback loop, reply mechanics), `send` (send/ring, reply-to, SENT vs QUEUED); bare `how-to` lists topics. Quickstart prompts point agents at it. |\n *57│| 13 | Quickstart rewrite: install (human) → **optional collapsed `<details>` pairing section** (human, elevated, create/join/status + cross-machine payoff line \"same prompts now work across machines\"; no captured two-machine example) → agent **prompt blocks** (copy-paste into agent sessions; agents run `spt how-to ready` / `how-to send` and follow) → offline-delivery demo → \"what just happened\". |\n  58│| 14 | Names: alice→**lea** (sender), bob→**sergey** (receiver). Scope: docs-site (Pages) + releases-repo README + `quickstart_e2e.rs`. Internal docs (CONTEXT.md, ADRs, plans — `ling`/`doyle`) unchanged. |\n  59│| 15 | README (releases-repo): quickstart bullet corrected to `ready`-based flow + pairing mention; status table \"send / ring / ready / poll\" loses `poll`. |\n *60│| 16 | New requirement family minted **before work starts** (TRACEABILITY rule 3): `REQ-SUBNET-*` (namespace surface · guided join e2e · node labels/addressable · elevation-gated membership), REQ-MSG-2 amended (poll merge), REQ-DOCS-x for `how-to`, REQ-PAIR-4/6 notes updated. Exact ids assigned at execution. |\n  61│\n  62│## Deliverables\n  63│\n    │...\n  78│one-ceremony-per-subnet riding it), CLI initiator (guided + non-interactive),\n *79│elevation gates on `create`/`join`, seed transfer + trust pinning + home\n  80│adoption on join, failure-mode UX. ADR-0005 amendment recorded.\n  81│\n  82│**D4 — docs**:\n    │...\n  99│1. Fresh two-machine setup completes via `spt subnet create` + `spt subnet join`\n*100│   only — no test harness, elevation prompts honored, `subnet status --nodes`\n 101│   shows both nodes labeled and online on both machines.\n 102│2. Quickstart runs as written: a human pastes the prompt blocks into two agent\n 103│   sessions; messages flow; the collapsed pairing section upgrades the same\n\n# M8-ACCEPTANCE-STEPS.md\n  14│Shorthands below:\n *15│- HF (run in an **elevated PowerShell** on HFENDULEAM):\n  16│  ```powershell\n  17│  $env:SPT_HOME=\"C:\\Users\\decid\\spt-accept\\home\"\n  18│  $spt=\"C:\\Users\\decid\\Documents\\projects\\spt-core\\target\\release\\spt.exe\"\n    │...\n  25│\n *26│> Legend: **[KIT-SSH]** I can run it. **[HF-elev]** you run, elevated.\n  27│> **[reboot]** disruptive to the CI runner — your call on timing.\n  28│\n  29│---\n    │...\n  37│|---|---|---|\n *38│| 1/3 | election + de-elevation (both OS) | ✅ (Win SPT_HOME-respawn bug found+fixed `f99dc25`) |\n  39│| 2 | Windows install | ✅ |\n  40│| 4 | old verbs gone / hot path | ✅ |\n  41│| 5 | attach/detach + prune | prune ✅ · **detach-flip DEFERRED** (subnet-scoped probe) |\n    │...\n  47│\n *48│**Fixes shipped this run** (dev-freeform): SPT_HOME survives Win de-elevation\n  49│respawn (`f99dc25`), clean `--nodes` labels (`b24dd61`), bounded liveness\n  50│probe + notice (`acbfbf4`).\n  51│**Deferred** (DEFERRED.md): subnet-scoped liveness probe (detach false-online),\n    │...\n  74│**Acceptance found a real REQ-INSTALL-6 bug; fixed and re-verified on hardware.**\n *75│- **De-elevation (criterion 3, KH-5.7): PASS** — `sudo spt` lands daemon+state\n  76│  under the elected user, never root (`strace` confirms `setgid`/`setuid`).\n  77│- **Election (criterion 1, decision 8): PASS after fix.** Root cause:\n *78│  `spt/src/main.rs` de-elevated early via `sudo_invoker()` (drop-only) for\n  79│  *every* command, before clap — shadowing BOTH daemon-side election guards\n  80│  (`Daemon::run` + `spawn_detached`), which then always saw `euid != 0` and\n  81│  no-op'd. So `/etc/spt-core/default-user` was never written by any path\n    │...\n  89│\n *90│## ✅ Criterion 1/3 (2026-06-07, HFENDULEAM) — Windows de-elevation bug found + FIXED + verified\n  91│\n *92│**Windows leg found a second REQ-HAZARD-ELEVATED-DAEMON-SPAWN bug; fixed and re-verified on hardware.**\n *93│- **Respawn (criterion 3): PASS** — elevated `daemon run` respawns de-elevated.\n  94│- **`daemon stop` ANOMALY → FIXED.** First run: `daemon stop` printed\n *95│  `DAEMON_NOT_RUNNING` while `Get-Process` showed two live unelevated `spt`.\n *96│  Root cause: the de-elevation respawn (`deelevate::create_with_token`)\n  97│  rebuilds the child env from the desktop-user token via\n  98│  `CreateEnvironmentBlock` (correct, to fix `%LOCALAPPDATA%`) but **dropped the\n  99│  invoker's `SPT_HOME`** — so the respawned daemon bound the seed-control\n    │...\n 102│  served the wrong home. `SPT_HOME` is the documented universe-relocation knob\n*103│  (`perch.rs`), so it must survive de-elevation. **Fix:** `apply_env_overrides`\n 104│  overlays `SPT_HOME` onto the rebuilt block (production = no `SPT_HOME` →\n 105│  unchanged). Verified: `daemon run` → respawn under accept-home → `daemon\n 106│  stop` → `DAEMON_STOPPED`, processes gone. Unit: `env_overlay_keeps_explicit_spt_home_alive`.\n    │...\n 111│Driven over SSH against `~/spt-accept-home`:\n*112│- **De-elevation (criterion 3, KH-5.7 safety): PASS.** Every clean\n 113│  `sudo … spt daemon run` lands the daemon + state under **reavus**\n 114│  (`identity/node.key` owned `reavus:reavus`), **never root**;\n 115│  `/root/.spt-core` is never created. Verified across file/pipe/tty captures.\n    │...\n 117│  `/etc/spt-core/default-user` is never written and the daemon prints no\n*118│  `DEELEVATED`/`ELECTED` line, in *any* capture mode (incl. forced tty), even\n*119│  though de-elevation provably happens and env (`SUDO_USER`/`SUDO_UID`) +\n 120│  `/etc` writability are confirmed fine. **Not correctness-blocking:**\n 121│  `daemon_target_user` falls back to the sudo invoker each run, so the daemon\n 122│  always lands under the right account whether or not the record persists —\n    │...\n 127│  - **strace evidence (kitsubito, deploy binary):** `sudo … spt daemon run`\n*128│    runs `setgid(1000)`+`setuid(1000)` (de-elevation confirmed at the syscall\n 129│    level) but performs **no `openat` of `/etc/spt-core/default-user`**\n 130│    (`write_election` never reached) and **no `write(2,…)` of\n*131│    `DEELEVATED`/`ELECTED`** — even though `SUDO_USER=reavus` is present in the\n 132│    env and `getent passwd reavus`→`1000` resolves (so neither the\n 133│    `default_name.is_empty()` nor the `user_by_name()==None` branch should be\n 134│    taken). The confirmed-on-a-real-terminal \"no output + no record\" rules out\n*135│    SSH suppression. **Next step: instrument `deelevate::daemon_target_user`\n 136│    branch-by-branch (eprintln per arm), rebuild, run under sudo** — the\n 137│    binary's actual branch is not what the source predicts; remote tracing\n*138│    can't see the daemon's own stdout. Non-blocking: de-elevation lands state\n 139│    under the invoker every run regardless of the record.\n 140│\n*141│## Criterion 1 + 3 — fresh-install election + de-elevation\n 142│\n\n# M8-ACCEPTANCE.md\n  11│\n *12│Driven over SSH (`reavus@kitsubito` w/ sudo, `decid@enlyzeam` elevated). D4\n  13│built on each from source; throwaway subnets cleaned up after.\n  14│\n  15│- **Criterion 2 — fresh Windows install (enlyzeam).** ✅ `install.ps1`\n    │...\n  36│- **Criterion 1 — Linux install legs (kitsubito).** ◐ partial. The installer\n *37│  legs all proved: unelevated install → ~/.local/bin + PATH + systemd **user\n  38│  service started** + **graceful printed sudo hints** for the symlink/linger\n  39│  (REQ-INSTALL-6/8); the `sudo ln` symlink made `sudo spt` resolve;\n  40│  `loginctl enable-linger` set. NOT a clean criterion-1: kitsubito is a *live\n  41│  SPT_DEV node*, so the fresh-account election + reboot-reachable legs want a\n *42│  clean user/box (election ladder + de-elevation are unit-covered;\n  43│  `/root` confirmed never contaminated). Bonus: `subnet leave` ran\n *44│  elevation-gated on both nodes (criterion 5's leave verb).\n  45│\n  46│### Bug found by acceptance (FIXED) — notif_id UNIQUE collision\n  47│\n    │...\n 107│\n*108│These require fresh installs / reboots / elevation prompts / a second or\n 109│third machine / enlyzeam's skewed clock — not performable from this bench.\n 110│Deploy a **D4 binary** to each rig node first (any new binary also self-heals\n 111│hfenduleam's v0.1.0-dead pump — D4 makes that class supervised + visible).\n    │...\n 116│   (never root); node is reachable **after a reboot** before any manual `spt`\n*117│   (systemd user unit + linger). → criteria 1, 3 (Linux de-elevation live).\n 118│\n*119│2. **Fresh Windows install** (elevated leg): inbound works with zero\n 120│   accumulated dev firewall rules — a join succeeds where M7's\n*121│   NO_SEED_HOLDER failed. Then the **unelevated-install** path: `spt subnet\n 122│   status` renders the blocked-inbound state honestly. → criteria 2, 3.\n 123│\n 124│5. **attach/detach + prune** on a live pair: `spt subnet detach <n>` makes a\n\n# M8-PLAN.md\n   4│> 2026-06-06 M7-closeout grills (nounification shape, subnet attachment verbs,\n  *5│> Linux elevation model); the scope-inclusion decisions (former §Open\n   6│> decisions A–G, plus H/I from the 2026-06-07 pump diagnosis) were ratified\n   7│> in the 2026-06-07 grill — see decisions 16–24. Requirements minted before\n   8│> work begins (TRACEABILITY rule 3).\n    │...\n  12│scoping starts after M8), and make the *installed* binary a first-class\n *13│citizen on both OSes: elevation that works on Linux, inbound that works on\n  14│Windows, a daemon that survives reboot. M8 is the last cheap moment for\n  15│surface breaks; after this, spt-claude-code freezes the verbs it consumes.\n  16│\n    │...\n  27│3. **The installer's \"next touch\" is already owed** three legs at once\n *28│   (Linux elevation, firewall registration, OS-service) — one coherent\n *29│   install/elevation deliverable instead of three drive-bys.\n  30│\n  31│## Root causes this milestone fixes\n  32│\n    │...\n  35│   operations have no noun home; `spt daemon` is a hidden verb.\n *36│2. **A user-scope install cannot be elevated on Linux.** The binary lives in\n  37│   `~/.local/bin` (not on sudo's `secure_path`), and `sudo spt` — when it\n  38│   resolves at all — runs the daemon in root's HOME universe. (CLI guidance\n  39│   band-aid shipped post-M7 @ `1359649`; the *model* — symlink + default\n    │...\n  71│| 5 | **`spt daemon stop\\|status`** noun; hidden `daemon` verb → `daemon run`. Agent-endpoint `shutdown` keeps its name (under `endpoint`). |\n *72│| 6 | **Subnet attachment verbs** (user spec 2026-06-06; CONTEXT §subnet attachment): `spt subnet detach <NAME> [--save]` / `attach <NAME> [--save]` — daemon keeps running, stops/starts advertising + connecting for that subnet; `--save` persists the startup default (renamed from `--auto`, user 2026-06-07 — clearer semantics). `spt subnet leave <NAME>` elevation-gated. |\n *73│| 7 | **Trust-row prune**: `spt subnet prune <node>` (or ratified final shape) — removes a dead identity's TRUST rows; elevation-gated (trust mutation = security surface). BIGNET's two 09ef831e rows are the live test case. |\n *74│| 8 | **Linux elevation model** (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch runs the daemon (and state) under that account, never root. Windows has no equivalent problem (same user profile). |\n  75│| 9 | No deprecation shims, same as M7 decision 1 — zero external CLI consumers until spt-claude-code exists. Breaking renames land clean. |\n *76│| 10 | New requirements minted **before work starts** (TRACEABILITY rule 3): `REQ-CLI-*` family (endpoint noun · daemon noun · hot-path preservation), `REQ-SUBNET-*` additions (attach/detach serve-state · prune · re-pair overwrite · status render), `REQ-INSTALL-*` additions (Linux elevation install leg · firewall registration · OS-service), polish reqs per scope ratification. Exact ids assigned at execution; inactive until their deliverable starts (rule 5). |\n  77│| 11 | **`subnet status` is daemon-aware** (user 2026-06-07): zero-subnet text becomes \"No subnets registered — this node is standalone.\" followed by a daemon-status-dependent blurb (daemon running vs not). Never implies messaging works while the daemon is down. |\n  78│| 12 | **Hint footer only on bare `spt subnet`** (user 2026-06-07): `spt subnet status` drops its \"hint:\" lines; the flagless `spt subnet` view keeps them (amends M7 decision 5's \"always printed\"). |\n *79│| 13 | **Per-machine re-pair trust overwrite** (user 2026-06-07): registry rows carry a stable per-machine identifier (NOT the MAC — it changes between wifi/ethernet; source per open decision G). A **successful pairing ceremony** presenting the same node label AND the same machine identifier as an existing trusted row is treated as a re-pair of that machine: the old identity's trust + registry rows are deleted automatically. Overwrite fires only inside a completed ceremony (TOTP + SPAKE2 + the join-sid...\n  80│| 14 | **Fast convergence covers daemon bringup too** (user 2026-06-07): D4's address seeding targets both cold starts — post-join (seed from the ceremony connection) and post-restart (persist + reload the pump's last-known peer addresses), so a restarted daemon reconnects in seconds, not 60s+. |\n  81│| 15 | **Fast-track advertisement on node state change** (user 2026-06-07): an endpoint coming online / going offline (ready-listener start/stop, rest-state transition, perch death) triggers an immediate `advertise_local` + peer push instead of waiting out the pump cadence. The cadence stays the steady-state floor; the event path is the fast lane. Field motivation: a re-listened endpoint stayed rendered `[0/1]` for up to a full cadence after coming online (2026-06-07 diagnosis tail). |\n  82│\n    │...\n  87│| 16 | A | **`spt subnet disband` DEFERRED.** `leave` covers the field need; adding `disband` later is non-breaking, so the adapter window doesn't force it. |\n *88│| 17 | B | **OS-service shape: Linux systemd *user* service + `loginctl enable-linger`** (linger enable rides the elevated install leg; daemon starts at boot pre-login, user-universe per KH 5.7, managed via `systemctl --user`). **Windows: scheduled task at-logon** (interactive session, no stored credentials, no session-0 isolation breaking terminal hosting). At-startup S4U variant = later opt-in if a headless Windows node ever exists. |\n  89│| 18 | C | **NTP IN SCOPE — trigger fired**: enlyzeam's system clock is known >1 min off (exceeds the ±1 TOTP window). Shape (C2): **TOTP offset-correction only** — query NTP at ceremony time (both sides), apply the offset to the TOTP calculation in-process; **system-clock fallback** when NTP unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop. Epoch/lease ordering stays monotonic-clock — untouche...\n  90│| 19 | D | **`xtask debug-converge` IN SCOPE** (builds in D4, activates REQ-UPD-6 int, `docs/DEBUG-CONVERGE-PLAN.md` is the spec). The 2026-06-07 pump diagnosis was the hand-rolled version of this table — the tool pays for itself; acceptance 6 gains instrumented evidence. |\n  91│| 20 | E | **Address seeding = narrow patch, concretized**: there is NO existing addr cache (production resolution is the id-only `PeerResolver`, `peerloop.rs`). New minimal `peer-addrs.json` in the identity dir (sibling of `pump-ops.json`): pubkey-hex → last-known iroh NodeAddr. Resolver consults it FIRST, falls back to id-only discovery on miss **or dial failure** (a stale addr never strands a peer). Two writers: pairing ceremony (both sides, from the live connection) + the pump on successful connect...\n    │...\n 111│pump + responder; persisted `--auto` startup flags in daemon config), the\n*112│all-attached banner's per-subnet states, `subnet leave` (elevation),\n*113│`subnet prune <node>` (elevation; kills the dead-identity dials; BIGNET's\n 114│09ef831e rows are the acceptance fixture). **Re-pair trust overwrite**\n 115│(decisions 13/22): machine identifier = hashed OS machine id (minted-UUID\n 116│fallback) riding the registry rows (additive serde-default, old rows parse\n    │...\n 122│\n*123│**D3 — install & elevation hardening (the installer's owed touch):**\n 124│Linux: install.sh symlink leg into `/usr/local/bin` (graceful when\n*125│unelevated: print the one-liner instead) + first-`sudo spt` default-account\n 126│election + KH 5.7 interplay verified (daemon + state under the elected\n*127│account, never root). Windows: elevated install leg registers the firewall\n 128│rule (`New-NetFirewallRule`, inbound UDP) **and** the daemon self-detects\n 129│blocked inbound → rendered as the \"no connection\" state in `subnet status` +\n*130│coming-online banner (covers user-scope installs that skip the elevated leg).\n 131│Both OSes: OS-service registration per decision 17 — Linux systemd user\n*132│service + `enable-linger` (linger rides the elevated install leg); Windows\n 133│scheduled task at-logon (REQ-INSTALL-1 third leg — reboot-survivable\n 134│daemon). Installer-script E2E rungs extended (`SPT_INSTALL_E2E` stays\n 135│hermetic — service/firewall legs env-gated off in CI, proven in acceptance\n    │...\n 171│   survives a reboot and is reachable before any manual `spt` invocation.\n*172│2. **Fresh Windows install** (elevated leg): inbound works with zero\n 173│   accumulated dev rules — a join succeeds where M7's NO_SEED_HOLDER failed;\n*174│   the unelevated-install path renders the blocked-inbound state honestly in\n 175│   `subnet status`.\n\n# MESH-D3-PLAN.md\n 33│- `tombstone(subnet, pubkey, stamp)` — add a tombstone (idempotent). **Dominates** the entry: `is_member`/`members_in` exclude it; **suppresses reinsert**: a later `merge_entry` of that pubkey re-records facts but `is_member` stays false while the tombstone stands.\n*34│- `clear_tombstone(subnet, pubkey) -> bool` — **clear-on-repair**: remove the tombstone (the deliberate elevated re-pair act); the entry, if present, becomes a member again.\n 35│- `is_tombstoned(subnet, pubkey) -> bool` · `is_member(subnet, pubkey) -> bool` (= entry present ∧ ¬tombstoned — **the admission predicate D5's gate will read**) · `members_in(subnet) -> impl Iterator<&RosterEntry>` (visible members only) · `roster_for(subnet) -> (Vec<RosterEntry>, Vec<Tombstone>)` (the slice handed at pairing — members + tombstones for one subnet).\n 36│\n*37│**Tombstone-convergence boundary (document in the module doc):** the tombstone is a grow-set element (propagates, dominates). `clear_tombstone` is a *local* elevated op — cross-node convergence of an *un*-tombstone is **not** D3's job: the compromised path re-pairs under a **fresh never-tombstoned key** (decision 10), so the old tombstone correctly stands; benign N-1 re-seed + timeboxed rotation propagation is **D7**. D3 tests clear-on-repair locally only.\n 38│\n 39│## Part B — `Frame::Seed` grows the roster (additive wire, `spt-net/pairing/wire.rs`)\n 40│\n\n# MESH-D7-PLAN.md\n 24│\n*25│List form (revoke several at once), **elevation-gated** (reuse `trust_mutation_refusal` — the leave/prune gate), **revoke-only** (own identity refuses; use `leave` to exit). For each resolved node (reuse `prune_candidates` resolution — hex/prefix/label):\n 26│\n 27│- **Tombstone now** in every subnet the node is a member of (`roster.tombstone`, the propagating grow-set marker — unlike prune's local-only tombstone). The tombstone gossips, so other members drop the revokee from their `is_member` gate within a roster round.\n 28│- **Schedule one rotation** at the close of a coalescing window (default 1h). Write/extend a pending-rotation marker (`identity/rotation-pending.json`: `{subnet → {deadline_ms, revokees:[hex]}}`). A second revoke inside the window joins the SAME entry (union the revokees, keep the earliest deadline) ⇒ one epoch bump.\n   │...\n 67│\n*68│- `spt subnet revoke <node>...` tombstones (propagating) + schedules one coalescing rotation; `--force-rotate-seed` rotates now; elevation-gated; own-identity refuses.\n 69│- A fired rotation bumps `seed_epoch`, force-drops revokees, and benign N-1 on-roster members auto-heal via a reseed_only connection that carries ONLY the new seed; N-2/off-roster/tombstoned are denied (→ re-pair).\n 70│- The seed never appears in roster/registry gossip; the per-conn proof grades exact/prior/stale (the deferred D2 epoch cross-check closed).\n 71│- `traceable-reqs check` clean with **REQ-MESH-4 `[impl,unit,int]`** activated; REQ-MESH-1/2/3/5 still satisfied; KH tests green; CI green (incl. the docs-drift gate — run `xtask gen` if `revoke --help` changes the CLI reference).","truncation":{"content":"# CHANGELOG.md\n 348|\n*349|A join-truth release: joining a subnet now tolerates a skewed or stepped system clock so it finds members it used to silently miss, no longer raises the elevated-permission prompt before a member is actually found, and can show the exact ceremony clock it is using — plus a loud warning when no time server can be reached.\n 350|\n 351|### Added\n 352|\n...\n 357|\n*358|- **Joining no longer raises the elevated-permission prompt before a member is found.** `spt subnet join` now asks for OS elevation only once it has actually located a subnet member, rather than up front — so a join that can't find anyone no longer pops an elevation prompt for nothing. The `--code` path is unchanged.\n 359|\n 360|### Fixed\n 361|\n...\n 919|- **`spt subnet join` shows a QR code + setup code on success.** After joining, scan the QR (or read the `otpauth://` code) to re-provision an authenticator app for the subnet.\n*920|- **Privilege-gated commands self-elevate, cross-platform.** When a command needs elevation, spt re-launches itself the right way for your system — a Windows UAC prompt, a Linux desktop `pkexec`/terminal `sudo`, or inline `sudo` in a terminal — and prints the exact command to run by hand when it can't.\n 921|- `spt spt` — ???\n 922|\n 923|### Changed\n...\n 1001|\n*1002|- **`spt subnet revoke <node>…`** — remove one or more machines from a subnet across the **whole fleet**, not just locally. It tells every member to drop the node within moments, then rotates the subnet's shared secret so the removed machine is locked out and must re-pair to come back. By default the rotation is batched at the end of a one-hour window — several revokes in that window share a single rotation, and any member that was briefly offline heals automatically across it. Pass `--force-rotate...\n 1003|- **`spt daemon start`** — bring the daemon up in the background, idempotently. When `spt` is installed as a service (the Linux per-user service, or the Windows logon task), `start` and `stop` now drive *that* service instead of a stray hand-started daemon — so the two never fight each other for the connection. `spt daemon start` on an already-running daemon just says so and does nothing.\n 1004|\n 1005|### Changed\n...\n 1009|- **`spt daemon stop` is service-aware.** If a managed service owns the daemon, `stop` asks the service manager to stop it cleanly (so it doesn't immediately restart), instead of signalling the process directly. A hand-started daemon still stops the same way as before.\n*1010|- **`spt daemon run` is now strictly foreground on every platform** — it stays attached to your terminal until you stop it (the form the installed service uses). For a background daemon, use `spt daemon start`. On Windows, running `daemon run` from an elevated shell now refuses with a hint rather than silently disappearing into the background.\n 1011|- **`spt daemon status` shows what manages the daemon** — whether a service owns it (and is active) or it was started by hand.\n 1012|\n 1013|### Performance\n...\n 1044|- **Automatic re-pair cleanup.** If you reinstall or regenerate a node's identity and pair it again from the same machine under the same name, its old, now-dead identity is removed automatically during pairing — no manual prune needed.\n*1045|- **Firewall setup on Windows.** The installer (when run elevated) now adds the inbound network rule `spt` needs so other nodes can reach you. If it wasn't added, `spt subnet status` and the \"coming online\" banner now tell you the rule is missing and print the exact command to add it.\n 1046|- **Starts on boot.** The installer now registers `spt` to start automatically — at login on Windows, and as a per-user service on Linux — so your node is reachable after a reboot without running a command first.\n 1047|\n 1048|### Changed\n...\n 1066|- **Messages from other agents now arrive properly formatted.** Incoming messages on the listener stream now include the full envelope with the sender's name, instead of showing as a raw, unwrapped line.\n*1067|- **`sudo spt …` now works on Linux user installs.** When `spt` is installed to your user directory, elevation guidance that said to \"run as administrator/root\" used to dead-end with `sudo: spt: command not found`. The installer now also makes `spt` reachable under `sudo`, and on an interactive terminal `spt` re-runs itself with `sudo` automatically; otherwise it prints a command that actually works.\n*1068|- **Elevated `spt` on Linux runs under your account, not root.** The first time you run an elevated `spt`, it asks once which account should own the daemon and its data, remembers that choice, and every later `sudo spt` runs the daemon and stores state under that account — never as root.\n*1069|- **`spt daemon stop` on Windows now finds the daemon it started.** A daemon launched through Windows' elevation prompt could end up using the wrong home directory, so `spt daemon stop` reported \"daemon not running\" while a daemon kept running. It now keeps the right home directory across that elevation step.\n*1070|- **Removed a confusing internal status line.** `spt` no longer prints the internal \"DEELEVATED: running as uid …\" notice during normal use.\n 1071|- **Stale node rows clear out on their own.** Nodes that haven't been heard from in a while are now removed from the listing automatically, so old/dead entries stop cluttering `spt subnet status`.\n 1072|\n 1073|## [0.1.1] - 2026-06-07\n\n# CI-SELFHOST-PLAN.md\n 21|\n*22|1. **Registration tokens** (repo admin):\n 23|   `gh api -X POST repos/SaberMage/spt-core/actions/runners/registration-token --jq .token`\n 24|2. **HFENDULEAM runner**: download actions-runner (win-x64), `config.cmd --url\n 25|   https://github.com/SaberMage/spt-core --token <T> --labels hfenduleam\n\n# CONTEXT.md\n 727|**seed rotation / revoke**:\n*728|Removing a member is **`spt subnet revoke <node>...`** (elevation-gated, revoke-only — *adding* a member never rotates; the joiner just receives the current seed at pairing). Two effects:\n 729|- **Immediate:** writes a **roster tombstone** per revoked pubkey (see *member roster*) — propagates over member connections, suppresses that pubkey under the roster's union-merge, and augments the inbound gate to **membership-proof ∧ ¬tombstoned** so the node can't reconnect-and-reinsert (it still holds the seed until rotation). Force-drops its connections.\n 730|- **Coalesced rotation:** the tombstone schedules **one** seed rotation (re-mint seed, bump the **seed-rotation epoch** — `SubnetRecord.epoch`, ADR-0005 #10; push the new seed confidentially over member-authenticated TLS connections, **never** in roster/registry gossip — the seed is not roster data) at the close of a **coalescing window (default 1 h)**. Further revokes within the window join the same rotation → **one epoch bump** however many nodes, keeping benign offliners inside the single-epoch ...\n 731|A completed re-pair ceremony for a tombstoned pubkey clears its tombstone (deliberate re-admit).\n...\n 766|\n*767|**Node-bound code fetch — and every subnet-membership mutation — is gated behind OS privilege elevation** (Windows UAC / Linux root-or-equivalent): retrieving a subnet's code *from the node*, minting a new subnet (`subnet create` — a seed reveal), and joining one (`subnet join` — enrolling the machine into a trust fabric) all require either hardware/elevated access OR an **elevated endpoint** (an agent whose process is elevated can surface it). The join gate exists because membership is a trust-b...\n 768|\n*769|**self-elevating re-launch (cross-platform)**:\n*770|<!-- [doc->REQ-ELEVATE-1] -->\n*771|When a gated command is run unelevated, spt does not just print \"run as administrator\" — it **re-launches itself with privilege** so the user reaches the result in one step. The path is chosen by a pure decision seam (`elevation::decide_elevation_path`) from the OS, the current elevation, and the environment: an **interactive Unix TTY** re-execs inline under `sudo`; a **Linux desktop without a TTY** prefers **`pkexec`** (native polkit GUI auth, clean stdio) and falls back to a **terminal-emulator** (`x...\n 772|\n 773|**node label**:\n 774|A human display name for a node, defaulting to the machine's OS hostname (re-checked at daemon startup; a hostname change updates the label). Advertised through the existing registry gossip so subnet views render `HFENDULEAM (bcead52b…)` instead of bare key hex. The pubkey remains the identity; the label is **addressable**: an `@node` qualifier accepts a label or a key-prefix (`ling@hfenduleam`). Labels are not unique — an ambiguous label follows the resolution policy's refuse-and-qualify rule (refus...\n...\n 832|**arriving-message envelope**:\n*833|Every message that arrives at a consumer — an agent over `api listen`/`spt ready`, an adapter's hook composer over `api poll`/`api worker-poll`, the cross-node WAN feed — is delivered as the canonical **`<EVENT type=\"…\" from=\"…\">body</EVENT>` envelope** (`spt-proto::event`, the ADR-0001 grammar; self-delimiting, `from` attribution, `<br>` body escaping, `<EVENT-PART>` chunking). It is the one format adapters parse on, identical across every **arriving-message** surface on an agent perch. The earl...\n 834|\n 835|**file-transfer progress**:\n 836|Every file transfer over the substrate is addressable and **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI) — at any point during the transfer. A substrate-wide requirement, not shell-specific (the Shell text+file channel inherits it).\n\n# DAEMON-LIFECYCLE-PLAN.md\n 29|- `crates/spt-daemon/src/daemon.rs`: `ensure_running()` → `spawn_detached()`\n*30|  (Linux fork-style; Windows `DETACHED_PROCESS | CREATE_NO_WINDOW`, de-elevated\n*31|  via `deelevate`). `request_stop()` = IPC stop.\n 32|- Installer-registered services:\n 33|  - Linux (`install.sh`): systemd **user** unit `~/.config/systemd/user/spt-daemon.service`,\n 34|    `ExecStart=<bin> daemon run`, `enabled`; `loginctl enable-linger` for pre-login boot.\n 35|  - Windows (`install.ps1`): at-logon **scheduled task** `spt-core daemon`\n*36|    (`schtasks /SC ONLOGON /TR \"<bin> daemon run\"`) — needs elevation to register.\n 37|\n 38|## Target model (consistent across platforms)\n 39|\n...\n 68|  - (a) **Promote to a real Windows Service** (`sc.exe create` / a small service\n*69|    host) → clean `start`/`stop` parity with systemd. Heavier; needs elevation\n 70|    + a service-mode entry in the binary.\n 71|  - (b) **Keep the scheduled task for boot-start; `start` = detached spawn,\n 72|    `stop` = IPC stop.** Simpler; honest asymmetry (Windows has no per-user\n...\n 84|- Audit why Windows `spt daemon run` goes invisible-background (likely the\n*85|  `Daemon::run()` de-elevation respawn / a `spawn_detached` on the `run` path).\n 86|  Make `run` strictly foreground on Windows — the detached/invisible behavior\n 87|  lives ONLY in `start`. Keep `CREATE_NO_WINDOW` for the `start` detached spawn,\n 88|  not for foreground `run`.\n...\n 111|- v0.2.0 already registered the systemd unit on kitsubito + the scheduled-task\n*112|  attempt on HFENDULEAM (HFENDULEAM's task needs an elevated re-run — still open).\n 113|- After this ships: re-roll to the fleet so `start`/`stop` drive the managed\n 114|  service everywhere; document the daemon-lifecycle UX in the CLI reference +\n 115|  CHANGELOG.\n\n# DAEMON-LIFECYCLE-VERIFICATION.md\n 49|\n*50|- REQ-DAEMON-7's elevated-`run`-refuses behavior is Windows-only (N/A on this\n 51|  rig); Linux `daemon run` is foreground by construction (the in-process\n*52|  privilege drop keeps it foreground), which the unit's `ExecStart` relies on.\n 53|- The internal `ensure_running` auto-start shares the exact `plan_start(false,\n 54|  detected) → ServiceStart → systemctl --user start` path proven in step 3; a\n 55|  dedicated CLI trigger was skipped to avoid mutating live node state.\n\n# M12-CANDIDATES.md\n 18|\n*19|2. **Subnet create/join QR + self-elevating window.** `spt subnet create/join/show-totp`:\n 20|   - Surfaces a **QR code** of the TOTP seed for the user to store → must spawn a\n 21|     window to display it (create AND join).\n 22|   - Behavior change (per operator sketch): runs normally if env is interactive +\n*23|     elevated; ELSE re-runs self in a self-elevating window with inline stdout\n*24|     (\"Elevated terminal launched. Accept the prompt and proceed there.\" → \"You can\n 25|     close this window.\", no auto-close).\n*26|   - Cross-platform elevation fallbacks: Windows = UAC self-elevate window;\n 27|     Linux+desktop = pkexec/polkit or x-terminal-emulator; Linux+TTY = inline sudo;\n 28|     headless/no-TTY = print exact command for the human (agent relays).\n*29|   - Scope elevation to only steps that need it (service-install / firewall /\n*30|     privileged-port) — subnet-create itself likely unprivileged.\n 31|   - Complementary downstream: spt-claude-code skill with create/join/show-code verbs.\n 32|\n 33|3. **spt-hosted HARNESS bringup + local user PTY attach (CLI). — CONFIRMED MISSING, TOP PRIORITY.**\n\n# M12-PLAN.md\n 56|\n*57|## Wave 4 — subnet UX (QR + self-elevation)\n 58|- **T4.1** `spt subnet create/join/show-totp`: surface a **QR code** of the TOTP seed (window\n 59|  on create AND join).\n*60|- **T4.2** Self-elevating behavior: run normally if interactive+elevated; else re-run self in\n*61|  a self-elevating window with inline stdout (\"Elevated terminal launched… / You can close\n 62|  this window.\"). Cross-platform: Win UAC window · Linux+desktop pkexec/polkit or\n 63|  x-terminal-emulator · Linux+TTY inline sudo · headless/no-TTY print command for the human.\n*64|  Scope elevation to only steps that need it (service/firewall/privileged-port; subnet-create\n*65|  itself likely unprivileged).\n 66|\n 67|## Wave 5 — whoami merge (small)\n 68|- **T5.1** `spt whoami` → alias for `spt endpoint list`; SELF-pin output additionally includes\n...\n 73|  cross-node attach proven; file-backed strings + adapter:profile fallback work; subnet QR +\n*74|  elevation on Win+Linux; whoami alias. Full suite green; `traceable-reqs check` EXIT=0;\n 75|  cross-subnet/PTY proof demonstrated (this is the spt-claude-code v1 acceptance dependency).\n 76|- Then unblock perri to begin `spt-claude-code` (see `../spt-claude-code/SCOPE.md`).\n\n# M12-W2.5-RULING.md\n 57|- Gate: `access_check(endpoint, origin, Unsolicited)` already decides who may control; taking\n*58|  control is the same gate (no elevated kick policy — if you may drive, you may take).\n 59|\n 60|### 6. Picker (W2 picker integration)\n 61|blue ■ = a controller is present (`driven_by` = controller node) **+ a viewer count** (\"controlled\n\n# M12-W4-PLAN.md\n*1|# M12 Wave 4 — subnet QR + self-elevating window — JIT plan\n 2|\n 3|> todlando 2026-06-14, after the W2 + follow-on batch gate (PASS, committed\n*4|> 6e76f62 / eb74b18). doyle: \"proceed to W4 subnet QR + self-elevating window;\n 5|> ping a design-check JIT like the others.\" **DESIGN-CHECK PENDING — build only\n*6|> after doyle's ruling.** The load-bearing unknown is T4.2 (self-elevating\n*7|> *window*): cross-platform + a security surface (spawning elevated processes /\n 8|> UAC). Spec: `M12-PLAN.md` Wave 4 + `M12-CANDIDATES.md`.\n 9|\n 10|## Scope (M12-PLAN.md Wave 4)\n...\n 12|  seed (on create AND join).\n*13|- **T4.2** **Self-elevating behavior**: run normally if interactive + elevated;\n*14|  else re-run self in a self-elevating window with inline stdout (\"Elevated\n 15|  terminal launched… / You can close this window\"). Cross-platform — Win UAC\n 16|  window · Linux+desktop pkexec/polkit or x-terminal-emulator · Linux+TTY inline\n*17|  sudo · headless/no-TTY print the command for the human. Scope elevation to only\n*18|  the steps that need it (service/firewall/privileged-port; subnet-create itself\n*19|  is unprivileged-to-run but reveals a secret, so it is gated).\n 20|\n 21|## What EXISTS today (investigation 2026-06-14)\n 22|- **QR already renders** (M7/M8, REQ-SUBNET-1): `render_qr` (cli.rs:2964) encodes\n...\n 26|  shipped** — the open question is whether anything is actually missing (Q1).\n*27|- **Elevation is PARTIALLY built** (`spt/src/elevation.rs`, REQ-PAIR-6 /\n 28|  REQ-HAZARD-SUDO-SECURE-PATH):\n 29|  - `current()` probe (Win token / Unix euid, fail-safe `Unknown`); `gate_for`\n 30|    (Show/Fallback); `fallback_message`.\n*31|  - `current_style()` (Unix `Sudo` / Win `AdminTerminal`); `rerun_command`\n*32|    (absolute-path sudo line, secure_path-proof); `should_auto_elevate` (Unix ∧\n*33|    interactive ∧ NotElevated).\n*34|  - `cli.rs::try_auto_elevate` ACTUALLY re-execs `sudo <abs-exe> <args>` on an\n*35|    **interactive Unix TTY** (the elevated child does the work; `main` de-elevates\n*36|    back via the 5.7 drop). `with_elevation_hint` prints the absolute-path sudo\n 37|    command otherwise. Consumed by `cmd_subnet_create` + `cmd_subnet_show_code`.\n 38|- **So the W4 T4.2 DELTA is the rest of the matrix:** today only **Unix +\n*39|  interactive-TTY** auto-elevates (inline sudo); **Windows prints \"run as\n*40|  administrator\" with NO actual UAC re-launch**; **Linux desktop without a TTY**\n 41|  (DISPLAY but no terminal) has no path; **headless** falls to the printed hint\n 42|  (Unix only). T4.2 = add the **Windows UAC window**, the **Linux desktop\n 43|  pkexec/terminal-emulator** path, and a **headless print** that covers Windows too.\n*44|- De-elevation seam exists (`spt-daemon::deelevate`, REQ-HAZARD-ELEVATED-DAEMON-SPAWN)\n*45|  — the elevated child drops back to the user for state safety; the\n\n# M12-W4-RULING.md\n*1|# M12 W4 — doyle design ruling (subnet QR + self-elevation)\n 2|\n*3|> doyle 2026-06-14, gating todlando's W4 JIT (M12-W4-PLAN.md). Privilege-escalation feature →\n 4|> security conditions stated explicitly + a mandatory hazard REQ. Primitives verified vs source\n 5|> before ruling.\n 6|\n 7|## Verified against source\n*8|- `elevation::rerun_command` enforces the **absolute exe path** (secure_path-proof, KH 5.10) — test `sudo_rerun_uses_absolute_path_not_bare_name` asserts `starts_with(\"sudo /\")`.\n*9|- `try_auto_elevate` (cli.rs:3065) re-execs `sudo <current_exe_str> <invocation_args>` — **verbatim args**, no widening.\n*10|- `ELEVATION_PROVEN` flag + `current()` (elevation.rs:164) short-circuit to `Elevated` once proven → **loop-guard foundation exists**.\n 11|- `render_qr` (cli.rs:2964) renders the `otpauth://` URI on create / show-code / join. T4.1 largely shipped.\n 12|\n 13|The matrix builds on sound primitives. Rulings:\n...\n 15|## Q1 — T4.1 QR: verify + close the join gap; \"window\" = inline terminal QR, NO GUI. ✓\n*16|Confirm `cmd_subnet_join` renders the QR (not only the code) post-join; add the `render_qr` call if missing, else no-op. No GUI milestone exists — reading \"window\" as a GUI surface would invent scope. (The \"window\" in the *milestone title* belongs to T4.2's elevated console, not a QR surface.)\n 17|\n*18|## Q2 — Windows UAC child-window: `runas` on the abs-exe DIRECTLY, child self-pauses, NO stdout marshaling.\n*19|`ShellExecuteW` with the `runas` verb on **`<abs-exe> <verbatim-args>`** — **not** a `cmd /k` wrapper (a `cmd /k` leaves an interactive *privileged shell* open = needless escalated surface). The elevated child detects it is the elevated re-launch, does the work, prints the result + \"You can close this window,\" and **pauses for a keypress** so the user reads it. The original process prints \"Elevated terminal launched…\" and exits 0.\n*20|- **Security (binding): never pipe / capture the elevated child's stdout back across the privilege boundary.** The unprivileged parent must not depend on privileged output. The child is self-contained.\n 21|- Verbatim args + absolute exe path (same secure-path discipline as the sudo leg, applied to the Windows launcher).\n 22|\n 23|## Q3 — Linux desktop order: confirmed; argv-array, never a shell string.\n 24|interactive-TTY → inline `sudo` (existing) → else `DISPLAY` ∧ `pkexec` → `pkexec` → else `DISPLAY` ∧ terminal-emulator → `x-terminal-emulator -e sudo …` (fallback list: x-terminal-emulator → gnome-terminal → konsole → xterm) → else → print the absolute-path sudo hint. pkexec preferred (native polkit GUI auth, clean stdio, no extra window).\n*25|- **Security (binding): pass argv as an ARRAY to pkexec / the terminal-emulator / runas — never a shell-interpolated command string.** A crafted id / path / arg must not be able to inject a second command. (`Command::new(...).args([...])`, not `sh -c \"<interpolated>\"`.)\n 26|\n 27|## Q4 — pure decision seam: APPROVED.\n*28|`decide_elevation_path(os, elevation, interactive_tty, has_display, has_pkexec, has_term_emulator) -> ElevatePath { AlreadyElevated, InlineSudo, UacWindow, Pkexec, TerminalEmulator, PrintHint }`, pure + unit-tested across the matrix; each variant's actual launch is the impure manual-verify leg (REQ-PAIR-6 precedent). Generalizes `should_auto_elevate`.\n*29|- **Required matrix coverage: `AlreadyElevated` returned whenever `current()==Elevated` (or `ELEVATION_PROVEN`), on EVERY os** — this is the loop-safety guarantee expressed in the testable seam. The elevated child re-enters, sees `Elevated`, and never re-elevates.\n 30|\n*31|## Q5 — REQ granularity: NEW REQ-ELEVATE-1. ✓\n*32|Cross-platform self-elevating re-launch is a distinct mechanism reused by every gated command (not subnet-specific). New **REQ-ELEVATE-1** (decision matrix + per-OS window launch + inline-notice UX). doc+impl+unit, **no int** (launch is manual-verify).\n 33|\n 34|## Q6 — Security posture: YES, add a hazard REQ. NON-NEGOTIABLE.\n*35|A privilege-escalation feature MUST carry a `REQ-HAZARD-*` with conformance tests. Add **REQ-HAZARD-SELF-ELEVATE** (required: unit; the hazard = a self-elevation that widens scope, hijacks PATH, injects via a shell string, or loops). The invariant:\n 36|\n*37|> Self-elevation re-runs the EXACT original invocation with the **absolute exe path** — never widening privilege scope, never adding/altering args, never via a PATH-resolved bare name, never via a shell-interpolated string (argv-array only); the elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates** (loop-safe via `AlreadyElevated`).\n 38|\n 39|Unit conformance (extends the existing abs-path test across the new launchers):\n*40|1. Every launcher's command uses the absolute exe path + verbatim args (Win `runas`, pkexec, terminal-emulator, sudo).\n*41|2. `decide_elevation_path` → `AlreadyElevated` when elevated, every os (loop-safety).\n 42|3. Launch argv is an array, not a shell string (no-injection — assert the constructed argv, no `sh -c`).\n 43|\n*44|The existing REQ-HAZARD-SUDO-SECURE-PATH covers the Unix abs-path-under-sudo facet; REQ-HAZARD-SELF-ELEVATE covers the cross-platform verbatim / no-widen / no-shell-injection / loop-safe facets. The \"print hint\" floor must also print the **absolute-path** command (copy-paste stays secure_path-proof).\n 45|\n 46|## Scope confirmation\n 47|W4 is mostly T4.2's Windows + Linux-desktop launch arms over the existing pure-seam pattern — smaller than W2. Agreed.\n...\n 49|## Binding gate conditions (I verify all three)\n*50|1. **Loop-safety** proven in the pure seam: `AlreadyElevated` short-circuit on every os.\n\n# M4-D2-WIRE-PLAN.md\n 109|D2f (multi-subnet ceremony inputs: code+name, rendezvous token, create-new names up front —\n*110|REQ-PAIR-4/5) → D2g (elevation-gated `spt pair show-totp` — REQ-PAIR-3/6) → D3 registry.\n\n# M4-D2f-PLAN.md\n 59|  library mechanism (create_subnet + named ceremony + seed transfer) is what D2f delivers.\n*60|- Elevation-gated code fetch (`spt pair show-totp`) → D2g (REQ-PAIR-6, REQ-PAIR-3).\n 61|\n 62|## Conventions (carried)\n 63|- NO `cargo fmt`. Tag `[impl->REQ-PAIR-{4,5}]` on real evidence, `[unit->…]` on tests.\n\n# M4-D2g-PLAN.md\n*1|# M4-D2g — elevation-gated per-subnet code fetch (JIT plan)\n 2|\n 3|**Status:** not started. D2f (multi-subnet + seed transfer) shipped & CI-green at `452358d`;\n 4|REQ-PAIR-4/5 `[impl,unit]`. D2g is the LAST D2 task: the local `spt pair show-totp` command\n*5|that surfaces a subnet's current code, **gated behind OS privilege elevation** (REQ-PAIR-6),\n 6|fetchable per-subnet from any paired node (REQ-PAIR-3). Local-only — no daemon/P2P, so\n 7|buildable now without the D4 net wiring.\n 8|\n...\n 11|`spt pair show-totp [--subnet <name> | --create-new]` prints the subnet's current 6-digit\n*12|code + `otpauth://` URI **only when the process is elevated** (Windows UAC / Linux\n 13|root-or-equiv); otherwise it refuses the node-bound fetch and points the user to their\n 14|authenticator app (where the seed was provisioned at pairing). Per-subnet because a node\n 15|holds N seeds (ADR-0006); `--create-new` mints+names a fresh subnet (sole seed-holder).\n...\n 18|\n*19|- **Real elevation gate** (todlando is available for admin-enabled testing on this machine).\n*20|  Linux: `libc::geteuid() == 0`. Windows: `GetTokenInformation(.., TokenElevation, ..)` via\n 21|  the `windows` crate. `cfg`-split; an unknown/unsupported platform returns `Unknown` →\n*22|  treated as NOT elevated (fail safe → app fallback). Keep deps leaf-local (the elevation\n 23|  util crate only), preserving the dependency-minimal core.\n*24|- **Where:** put `is_elevated()` / `Elevation` in the `spt` binary (CLI-adjacent, e.g.\n*25|  `crates/spt/src/elevation.rs`) — it gates a CLI command, and only the binary need pull the\n 26|  OS deps. The code fetch itself reads `spt-store::SubnetStore` (already there).\n 27|- **No-flag behavior:** if the node holds exactly one subnet, use it; if several, **refuse +\n 28|  require `--subnet`** (no guessing — same ethos as D3c addressing). Zero subnets → tell the\n...\n 34|\n*35|1. **`elevation.rs`** (REQ-PAIR-6) — `enum Elevation { Elevated, NotElevated, Unknown }` +\n*36|   `fn current() -> Elevation`. `#[cfg(unix)]` libc geteuid; `#[cfg(windows)]` windows-crate\n*37|   TokenElevation; else Unknown. Unit-test the mapping logic where possible (the OS call\n 38|   itself is environment-dependent — gate behind a thin testable seam: a function taking the\n*39|   raw elevated-bool and producing the gate decision + message).\n 40|2. **`Cmd::Pair { ShowTotp { subnet, create_new } }`** in `cli.rs` — clap subcommand, stable\n 41|   arg shapes (REQ-MSG-2 style), parse tests.\n 42|3. **Handler** (REQ-PAIR-3) — load SubnetStore; resolve subnet (flag / single / refuse);\n*43|   `--create-new` → `create_subnet`; gate on `Elevation::current()`; elevated → print code\n 44|   (`seed.code_at(now)`) + `otpauth_uri`; not → print the authenticator-app fallback message.\n*45|   Exit codes: success 0; ambiguous/unknown-subnet = usage error; not-elevated = a distinct\n 46|   documented non-zero (the fetch was refused, not failed).\n 47|4. **Activate REQ-PAIR-3, REQ-PAIR-6** in `traceable-reqs.toml` (`[impl,unit]`; the live\n 48|   OS-gate end-to-end is hard to unit-test hermetically — cover the decision/seam in unit,\n*49|   note real-elevation verification is manual via todlando / D9).\n 50|\n 51|## Tests\n*52|- elevation: the gate-decision seam (elevated bool → show vs fallback) both ways; Unknown →\n 53|  fallback; the fallback message names the authenticator app.\n 54|- cli parse: `show-totp`, `--subnet x`, `--create-new`, mutually-exclusive flag rejection.\n*55|- handler (inject SubnetStore + now + a faked elevation): single-subnet auto-select; multi\n 56|  refuses without `--subnet`; `--create-new` mints+shows; code matches `seed.code_at(now)`;\n*57|  not-elevated path shows fallback + no code leak.\n 58|\n 59|## NOT in D2g\n*60|- Elevated **agent endpoint** path (an elevated agent surfacing the code over IPC) → needs\n*61|  the daemon/endpoint ACL (D4/D5); D2g does the node-bound OS-privilege path only.\n 62|- QR image rendering → GUI/later.\n 63|- REQ-PAIR-7 subnet icon → GUI (D9).\n 64|\n\n# M4-PLAN.md\n 117|  `H(subnet-name ‖ TOTP-epoch)`; create-new names the subnet up front. · **REQ-PAIR-5, REQ-PAIR-4**\n*118|- **D2g** Elevation-gated per-subnet code fetch (Win UAC / Linux root or elevated agent\n 119|  endpoint; else authenticator app). `spt pair show-totp [--subnet|--create-new]`. · **REQ-PAIR-6, REQ-PAIR-3**\n 120|- **Reqs:** REQ-PAIR-1..6. · **New hazard reqs to register first (rule 3):** add\n 121|  `REQ-HAZARD-PAIR-TRANSCRIPT-BIND` (#12), `REQ-HAZARD-PAIR-SEED-ROTATION` (#10),\n\n# M7-PLAN.md\n 4|> hardware: fresh two-machine setup via `subnet create` + the user's own\n*5|> fully-guided `subnet join` (no test harness, elevation prompts honored),\n 6|> `subnet status --nodes` labeled+online on both machines, cross-machine\n 7|> `SENT(WAN)` proven in both directions (HFENDULEAM ↔ gravity-linux), and the\n 8|> rig's product-surface join rung carries the pairing `int` evidence. The\n 9|> acceptance run surfaced two publish-blocking field bugs, both fixed before\n*10|> closeout: elevated daemon spawn (KNOWN-HAZARDS 5.7,\n*11|> REQ-HAZARD-ELEVATED-DAEMON-SPAWN — de-elevation seam) and immortal ghost\n 12|> registry rows (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS —\n 13|> silent-peer eviction), plus the AMBIGUOUS-render copy-paste-targets fix.\n*14|> Session harvest (Linux elevation model, NTP-first clock, firewall\n 15|> registration, post-join address seeding, trust-prune verb, M8\n 16|> nounification spec) is logged in `docs/DEFERRED.md`.\n 17|\n...\n 52|| 8 | **Responder always-on** in every member daemon (pre-trust pairing ALPN), per ADR-0005's one-sided-UX intent; the ADR red-team #11 subnet-global rate limiter is the standing-listener guard. No arming step. |\n*53|| 9 | **Elevation gates** (existing `EXIT_NOT_ELEVATED=3` convention): `subnet create` (seed reveal) and `subnet join` (trust-boundary mutation — an unprivileged process must not enroll the machine into an attacker's subnet) and `show-totp` (already gated). `status` ungated (read-only, no secrets). → ADR-0005 amendment, CONTEXT.md updated. |\n 54|| 10 | Guided join flow (joiner side, interactive when args missing): prompt name → prompt code → LAN+relay rendezvous search → ceremony → \"Joined \\\"home\\\" — 2 nodes: …\". Wrong code → clean re-prompt (fresh window), bounded attempts, rate-limit messages surfaced honestly. No-seed-holder-found → actionable hints (member online? same name? clock sync?). First-join home adoption (REQ-INST-15) fires on join as on create. |\n 55|| 11 | `subnet create` output: created-confirmation, current code, `otpauth://` URI, **terminal QR render** (unicode blocks; small `qrcode`-class crate), \"run `spt subnet join <name>` on the other machine\" hint. |\n 56|| 12 | **`spt how-to <topic>`**: in-binary, task-oriented agent instructions (anti-drift: single source = binary; docs-site never duplicates the content). Visible command, **Agent commands** group. Topics v1: `ready` (background-task guidance, `--once` fallback loop, reply mechanics), `send` (send/ring, reply-to, SENT vs QUEUED); bare `how-to` lists topics. Quickstart prompts point agents at it. |\n*57|| 13 | Quickstart rewrite: install (human) → **optional collapsed `<details>` pairing section** (human, elevated, create/join/status + cross-machine payoff line \"same prompts now work across machines\"; no captured two-machine example) → agent **prompt blocks** (copy-paste into agent sessions; agents run `spt how-to ready` / `how-to send` and follow) → offline-delivery demo → \"what just happened\". |\n 58|| 14 | Names: alice→**lea** (sender), bob→**sergey** (receiver). Scope: docs-site (Pages) + releases-repo README + `quickstart_e2e.rs`. Internal docs (CONTEXT.md, ADRs, plans — `ling`/`doyle`) unchanged. |\n 59|| 15 | README (releases-repo): quickstart bullet corrected to `ready`-based flow + pairing mention; status table \"send / ring / ready / poll\" loses `poll`. |\n*60|| 16 | New requirement family minted **before work starts** (TRACEABILITY rule 3): `REQ-SUBNET-*` (namespace surface · guided join e2e · node labels/addressable · elevation-gated membership), REQ-MSG-2 amended (poll merge), REQ-DOCS-x for `how-to`, REQ-PAIR-4/6 notes updated. Exact ids assigned at execution. |\n 61|\n 62|## Deliverables\n 63|\n...\n 78|one-ceremony-per-subnet riding it), CLI initiator (guided + non-interactive),\n*79|elevation gates on `create`/`join`, seed transfer + trust pinning + home\n 80|adoption on join, failure-mode UX. ADR-0005 amendment recorded.\n 81|\n 82|**D4 — docs**:\n...\n 99|1. Fresh two-machine setup completes via `spt subnet create` + `spt subnet join`\n*100|   only — no test harness, elevation prompts honored, `subnet status --nodes`\n 101|   shows both nodes labeled and online on both machines.\n 102|2. Quickstart runs as written: a human pastes the prompt blocks into two agent\n 103|   sessions; messages flow; the collapsed pairing section upgrades the same\n\n# M8-ACCEPTANCE-STEPS.md\n 14|Shorthands below:\n*15|- HF (run in an **elevated PowerShell** on HFENDULEAM):\n 16|  ```powershell\n 17|  $env:SPT_HOME=\"C:\\Users\\decid\\spt-accept\\home\"\n 18|  $spt=\"C:\\Users\\decid\\Documents\\projects\\spt-core\\target\\release\\spt.exe\"\n...\n 25|\n*26|> Legend: **[KIT-SSH]** I can run it. **[HF-elev]** you run, elevated.\n 27|> **[reboot]** disruptive to the CI runner — your call on timing.\n 28|\n 29|---\n...\n 37||---|---|---|\n*38|| 1/3 | election + de-elevation (both OS) | ✅ (Win SPT_HOME-respawn bug found+fixed `f99dc25`) |\n 39|| 2 | Windows install | ✅ |\n 40|| 4 | old verbs gone / hot path | ✅ |\n 41|| 5 | attach/detach + prune | prune ✅ · **detach-flip DEFERRED** (subnet-scoped probe) |\n...\n 47|\n*48|**Fixes shipped this run** (dev-freeform): SPT_HOME survives Win de-elevation\n 49|respawn (`f99dc25`), clean `--nodes` labels (`b24dd61`), bounded liveness\n 50|probe + notice (`acbfbf4`).\n 51|**Deferred** (DEFERRED.md): subnet-scoped liveness probe (detach false-online),\n...\n 74|**Acceptance found a real REQ-INSTALL-6 bug; fixed and re-verified on hardware.**\n*75|- **De-elevation (criterion 3, KH-5.7): PASS** — `sudo spt` lands daemon+state\n 76|  under the elected user, never root (`strace` confirms `setgid`/`setuid`).\n 77|- **Election (criterion 1, decision 8): PASS after fix.** Root cause:\n*78|  `spt/src/main.rs` de-elevated early via `sudo_invoker()` (drop-only) for\n 79|  *every* command, before clap — shadowing BOTH daemon-side election guards\n 80|  (`Daemon::run` + `spawn_detached`), which then always saw `euid != 0` and\n 81|  no-op'd. So `/etc/spt-core/default-user` was never written by any path\n...\n 89|\n*90|## ✅ Criterion 1/3 (2026-06-07, HFENDULEAM) — Windows de-elevation bug found + FIXED + verified\n 91|\n*92|**Windows leg found a second REQ-HAZARD-ELEVATED-DAEMON-SPAWN bug; fixed and re-verified on hardware.**\n*93|- **Respawn (criterion 3): PASS** — elevated `daemon run` respawns de-elevated.\n 94|- **`daemon stop` ANOMALY → FIXED.** First run: `daemon stop` printed\n*95|  `DAEMON_NOT_RUNNING` while `Get-Process` showed two live unelevated `spt`.\n*96|  Root cause: the de-elevation respawn (`deelevate::create_with_token`)\n 97|  rebuilds the child env from the desktop-user token via\n 98|  `CreateEnvironmentBlock` (correct, to fix `%LOCALAPPDATA%`) but **dropped the\n 99|  invoker's `SPT_HOME`** — so the respawned daemon bound the seed-control\n...\n 102|  served the wrong home. `SPT_HOME` is the documented universe-relocation knob\n*103|  (`perch.rs`), so it must survive de-elevation. **Fix:** `apply_env_overrides`\n 104|  overlays `SPT_HOME` onto the rebuilt block (production = no `SPT_HOME` →\n 105|  unchanged). Verified: `daemon run` → respawn under accept-home → `daemon\n 106|  stop` → `DAEMON_STOPPED`, processes gone. Unit: `env_overlay_keeps_explicit_spt_home_alive`.\n...\n 111|Driven over SSH against `~/spt-accept-home`:\n*112|- **De-elevation (criterion 3, KH-5.7 safety): PASS.** Every clean\n 113|  `sudo … spt daemon run` lands the daemon + state under **reavus**\n 114|  (`identity/node.key` owned `reavus:reavus`), **never root**;\n 115|  `/root/.spt-core` is never created. Verified across file/pipe/tty captures.\n...\n 117|  `/etc/spt-core/default-user` is never written and the daemon prints no\n*118|  `DEELEVATED`/`ELECTED` line, in *any* capture mode (incl. forced tty), even\n*119|  though de-elevation provably happens and env (`SUDO_USER`/`SUDO_UID`) +\n 120|  `/etc` writability are confirmed fine. **Not correctness-blocking:**\n 121|  `daemon_target_user` falls back to the sudo invoker each run, so the daemon\n 122|  always lands under the right account whether or not the record persists —\n...\n 127|  - **strace evidence (kitsubito, deploy binary):** `sudo … spt daemon run`\n*128|    runs `setgid(1000)`+`setuid(1000)` (de-elevation confirmed at the syscall\n 129|    level) but performs **no `openat` of `/etc/spt-core/default-user`**\n 130|    (`write_election` never reached) and **no `write(2,…)` of\n*131|    `DEELEVATED`/`ELECTED`** — even though `SUDO_USER=reavus` is present in the\n 132|    env and `getent passwd reavus`→`1000` resolves (so neither the\n 133|    `default_name.is_empty()` nor the `user_by_name()==None` branch should be\n 134|    taken). The confirmed-on-a-real-terminal \"no output + no record\" rules out\n*135|    SSH suppression. **Next step: instrument `deelevate::daemon_target_user`\n 136|    branch-by-branch (eprintln per arm), rebuild, run under sudo** — the\n 137|    binary's actual branch is not what the source predicts; remote tracing\n*138|    can't see the daemon's own stdout. Non-blocking: de-elevation lands state\n 139|    under the invoker every run regardless of the record.\n 140|\n*141|## Criterion 1 + 3 — fresh-install election + de-elevation\n 142|\n\n# M8-ACCEPTANCE.md\n 11|\n*12|Driven over SSH (`reavus@kitsubito` w/ sudo, `decid@enlyzeam` elevated). D4\n 13|built on each from source; throwaway subnets cleaned up after.\n 14|\n 15|- **Criterion 2 — fresh Windows install (enlyzeam).** ✅ `install.ps1`\n...\n 36|- **Criterion 1 — Linux install legs (kitsubito).** ◐ partial. The installer\n*37|  legs all proved: unelevated install → ~/.local/bin + PATH + systemd **user\n 38|  service started** + **graceful printed sudo hints** for the symlink/linger\n 39|  (REQ-INSTALL-6/8); the `sudo ln` symlink made `sudo spt` resolve;\n 40|  `loginctl enable-linger` set. NOT a clean criterion-1: kitsubito is a *live\n 41|  SPT_DEV node*, so the fresh-account election + reboot-reachable legs want a\n*42|  clean user/box (election ladder + de-elevation are unit-covered;\n 43|  `/root` confirmed never contaminated). Bonus: `subnet leave` ran\n*44|  elevation-gated on both nodes (criterion 5's leave verb).\n 45|\n 46|### Bug found by acceptance (FIXED) — notif_id UNIQUE collision\n 47|\n...\n 107|\n*108|These require fresh installs / reboots / elevation prompts / a second or\n 109|third machine / enlyzeam's skewed clock — not performable from this bench.\n 110|Deploy a **D4 binary** to each rig node first (any new binary also self-heals\n 111|hfenduleam's v0.1.0-dead pump — D4 makes that class supervised + visible).\n...\n 116|   (never root); node is reachable **after a reboot** before any manual `spt`\n*117|   (systemd user unit + linger). → criteria 1, 3 (Linux de-elevation live).\n 118|\n*119|2. **Fresh Windows install** (elevated leg): inbound works with zero\n 120|   accumulated dev firewall rules — a join succeeds where M7's\n*121|   NO_SEED_HOLDER failed. Then the **unelevated-install** path: `spt subnet\n 122|   status` renders the blocked-inbound state honestly. → criteria 2, 3.\n 123|\n 124|5. **attach/detach + prune** on a live pair: `spt subnet detach <n>` makes a\n\n# M8-PLAN.md\n 4|> 2026-06-06 M7-closeout grills (nounification shape, subnet attachment verbs,\n*5|> Linux elevation model); the scope-inclusion decisions (former §Open\n 6|> decisions A–G, plus H/I from the 2026-06-07 pump diagnosis) were ratified\n 7|> in the 2026-06-07 grill — see decisions 16–24. Requirements minted before\n 8|> work begins (TRACEABILITY rule 3).\n...\n 12|scoping starts after M8), and make the *installed* binary a first-class\n*13|citizen on both OSes: elevation that works on Linux, inbound that works on\n 14|Windows, a daemon that survives reboot. M8 is the last cheap moment for\n 15|surface breaks; after this, spt-claude-code freezes the verbs it consumes.\n 16|\n...\n 27|3. **The installer's \"next touch\" is already owed** three legs at once\n*28|   (Linux elevation, firewall registration, OS-service) — one coherent\n*29|   install/elevation deliverable instead of three drive-bys.\n 30|\n 31|## Root causes this milestone fixes\n 32|\n...\n 35|   operations have no noun home; `spt daemon` is a hidden verb.\n*36|2. **A user-scope install cannot be elevated on Linux.** The binary lives in\n 37|   `~/.local/bin` (not on sudo's `secure_path`), and `sudo spt` — when it\n 38|   resolves at all — runs the daemon in root's HOME universe. (CLI guidance\n 39|   band-aid shipped post-M7 @ `1359649`; the *model* — symlink + default\n...\n 71|| 5 | **`spt daemon stop\\|status`** noun; hidden `daemon` verb → `daemon run`. Agent-endpoint `shutdown` keeps its name (under `endpoint`). |\n*72|| 6 | **Subnet attachment verbs** (user spec 2026-06-06; CONTEXT §subnet attachment): `spt subnet detach <NAME> [--save]` / `attach <NAME> [--save]` — daemon keeps running, stops/starts advertising + connecting for that subnet; `--save` persists the startup default (renamed from `--auto`, user 2026-06-07 — clearer semantics). `spt subnet leave <NAME>` elevation-gated. |\n*73|| 7 | **Trust-row prune**: `spt subnet prune <node>` (or ratified final shape) — removes a dead identity's TRUST rows; elevation-gated (trust mutation = security surface). BIGNET's two 09ef831e rows are the live test case. |\n*74|| 8 | **Linux elevation model** (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch runs the daemon (and state) under that account, never root. Windows has no equivalent problem (same user profile). |\n 75|| 9 | No deprecation shims, same as M7 decision 1 — zero external CLI consumers until spt-claude-code exists. Breaking renames land clean. |\n*76|| 10 | New requirements minted **before work starts** (TRACEABILITY rule 3): `REQ-CLI-*` family (endpoint noun · daemon noun · hot-path preservation), `REQ-SUBNET-*` additions (attach/detach serve-state · prune · re-pair overwrite · status render), `REQ-INSTALL-*` additions (Linux elevation install leg · firewall registration · OS-service), polish reqs per scope ratification. Exact ids assigned at execution; inactive until their deliverable starts (rule 5). |\n 77|| 11 | **`subnet status` is daemon-aware** (user 2026-06-07): zero-subnet text becomes \"No subnets registered — this node is standalone.\" followed by a daemon-status-dependent blurb (daemon running vs not). Never implies messaging works while the daemon is down. |\n 78|| 12 | **Hint footer only on bare `spt subnet`** (user 2026-06-07): `spt subnet status` drops its \"hint:\" lines; the flagless `spt subnet` view keeps them (amends M7 decision 5's \"always printed\"). |\n*79|| 13 | **Per-machine re-pair trust overwrite** (user 2026-06-07): registry rows carry a stable per-machine identifier (NOT the MAC — it changes between wifi/ethernet; source per open decision G). A **successful pairing ceremony** presenting the same node label AND the same machine identifier as an existing trusted row is treated as a re-pair of that machine: the old identity's trust + registry rows are deleted automatically. Overwrite fires only inside a completed ceremony (TOTP + SPAKE2 + the join-sid...\n 80|| 14 | **Fast convergence covers daemon bringup too** (user 2026-06-07): D4's address seeding targets both cold starts — post-join (seed from the ceremony connection) and post-restart (persist + reload the pump's last-known peer addresses), so a restarted daemon reconnects in seconds, not 60s+. |\n 81|| 15 | **Fast-track advertisement on node state change** (user 2026-06-07): an endpoint coming online / going offline (ready-listener start/stop, rest-state transition, perch death) triggers an immediate `advertise_local` + peer push instead of waiting out the pump cadence. The cadence stays the steady-state floor; the event path is the fast lane. Field motivation: a re-listened endpoint stayed rendered `[0/1]` for up to a full cadence after coming online (2026-06-07 diagnosis tail). |\n 82|\n...\n 87|| 16 | A | **`spt subnet disband` DEFERRED.** `leave` covers the field need; adding `disband` later is non-breaking, so the adapter window doesn't force it. |\n*88|| 17 | B | **OS-service shape: Linux systemd *user* service + `loginctl enable-linger`** (linger enable rides the elevated install leg; daemon starts at boot pre-login, user-universe per KH 5.7, managed via `systemctl --user`). **Windows: scheduled task at-logon** (interactive session, no stored credentials, no session-0 isolation breaking terminal hosting). At-startup S4U variant = later opt-in if a headless Windows node ever exists. |\n 89|| 18 | C | **NTP IN SCOPE — trigger fired**: enlyzeam's system clock is known >1 min off (exceeds the ±1 TOTP window). Shape (C2): **TOTP offset-correction only** — query NTP at ceremony time (both sides), apply the offset to the TOTP calculation in-process; **system-clock fallback** when NTP unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop. Epoch/lease ordering stays monotonic-clock — untouche...\n 90|| 19 | D | **`xtask debug-converge` IN SCOPE** (builds in D4, activates REQ-UPD-6 int, `docs/DEBUG-CONVERGE-PLAN.md` is the spec). The 2026-06-07 pump diagnosis was the hand-rolled version of this table — the tool pays for itself; acceptance 6 gains instrumented evidence. |\n 91|| 20 | E | **Address seeding = narrow patch, concretized**: there is NO existing addr cache (production resolution is the id-only `PeerResolver`, `peerloop.rs`). New minimal `peer-addrs.json` in the identity dir (sibling of `pump-ops.json`): pubkey-hex → last-known iroh NodeAddr. Resolver consults it FIRST, falls back to id-only discovery on miss **or dial failure** (a stale addr never strands a peer). Two writers: pairing ceremony (both sides, from the live connection) + the pump on successful connect...\n...\n 111|pump + responder; persisted `--auto` startup flags in daemon config), the\n*112|all-attached banner's per-subnet states, `subnet leave` (elevation),\n*113|`subnet prune <node>` (elevation; kills the dead-identity dials; BIGNET's\n 114|09ef831e rows are the acceptance fixture). **Re-pair trust overwrite**\n 115|(decisions 13/22): machine identifier = hashed OS machine id (minted-UUID\n 116|fallback) riding the registry rows (additive serde-default, old rows parse\n...\n 122|\n*123|**D3 — install & elevation hardening (the installer's owed touch):**\n 124|Linux: install.sh symlink leg into `/usr/local/bin` (graceful when\n*125|unelevated: print the one-liner instead) + first-`sudo spt` default-account\n 126|election + KH 5.7 interplay verified (daemon + state under the elected\n*127|account, never root). Windows: elevated install leg registers the firewall\n 128|rule (`New-NetFirewallRule`, inbound UDP) **and** the daemon self-detects\n 129|blocked inbound → rendered as the \"no connection\" state in `subnet status` +\n*130|coming-online banner (covers user-scope installs that skip the elevated leg).\n 131|Both OSes: OS-service registration per decision 17 — Linux systemd user\n*132|service + `enable-linger` (linger rides the elevated install leg); Windows\n 133|scheduled task at-logon (REQ-INSTALL-1 third leg — reboot-survivable\n 134|daemon). Installer-script E2E rungs extended (`SPT_INSTALL_E2E` stays\n 135|hermetic — service/firewall legs env-gated off in CI, proven in acceptance\n...\n 171|   survives a reboot and is reachable before any manual `spt` invocation.\n*172|2. **Fresh Windows install** (elevated leg): inbound works with zero\n 173|   accumulated dev rules — a join succeeds where M7's NO_SEED_HOLDER failed;\n*174|   the unelevated-install path renders the blocked-inbound state honestly in\n 175|   `subnet status`.\n\n# MESH-D3-PLAN.md\n 33|- `tombstone(subnet, pubkey, stamp)` — add a tombstone (idempotent). **Dominates** the entry: `is_member`/`members_in` exclude it; **suppresses reinsert**: a later `merge_entry` of that pubkey re-records facts but `is_member` stays false while the tombstone stands.\n*34|- `clear_tombstone(subnet, pubkey) -> bool` — **clear-on-repair**: remove the tombstone (the deliberate elevated re-pair act); the entry, if present, becomes a member again.\n 35|- `is_tombstoned(subnet, pubkey) -> bool` · `is_member(subnet, pubkey) -> bool` (= entry present ∧ ¬tombstoned — **the admission predicate D5's gate will read**) · `members_in(subnet) -> impl Iterator<&RosterEntry>` (visible members only) · `roster_for(subnet) -> (Vec<RosterEntry>, Vec<Tombstone>)` (the slice handed at pairing — members + tombstones for one subnet).\n 36|\n*37|**Tombstone-convergence boundary (document in the module doc):** the tombstone is a grow-set element (propagates, dominates). `clear_tombstone` is a *local* elevated op — cross-node convergence of an *un*-tombstone is **not** D3's job: the compromised path re-pairs under a **fresh never-tombstoned key** (decision 10), so the old tombstone correctly stands; benign N-1 re-seed + timeboxed rotation propagation is **D7**. D3 tests clear-on-repair locally only.\n 38|\n 39|## Part B — `Frame::Seed` grows the roster (additive wire, `spt-net/pairing/wire.rs`)\n 40|\n\n# MESH-D7-PLAN.md\n 24|\n*25|List form (revoke several at once), **elevation-gated** (reuse `trust_mutation_refusal` — the leave/prune gate), **revoke-only** (own identity refuses; use `leave` to exit). For each resolved node (reuse `prune_candidates` resolution — hex/prefix/label):\n 26|\n 27|- **Tombstone now** in every subnet the node is a member of (`roster.tombstone`, the propagating grow-set marker — unlike prune's local-only tombstone). The tombstone gossips, so other members drop the revokee from their `is_member` gate within a roster round.\n 28|- **Schedule one rotation** at the close of a coalescing window (default 1h). Write/extend a pending-rotation marker (`identity/rotation-pending.json`: `{subnet → {deadline_ms, revokees:[hex]}}`). A second revoke inside the window joins the SAME entry (union the revokees, keep the earliest deadline) ⇒ one epoch bump.\n...\n 67|\n*68|- `spt subnet revoke <node>...` tombstones (propagating) + schedules one coalescing rotation; `--force-rotate-seed` rotates now; elevation-gated; own-identity refuses.\n 69|- A fired rotation bumps `seed_epoch`, force-drops revokees, and benign N-1 on-roster members auto-heal via a reseed_only connection that carries ONLY the new seed; N-2/off-roster/tombstoned are denied (→ re-pair).\n 70|- The seed never appears in roster/registry gossip; the per-conn proof grades exact/prior/stale (the deferred D2 epoch cross-check closed).","truncated":true,"truncatedBy":"bytes","totalLines":592,"totalBytes":51508,"outputLines":589,"outputBytes":51187,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":592,"totalBytes":51508,"outputLines":589,"outputBytes":51187,"shownRange":{"start":1,"end":589},"nextOffset":590}}},"isError":false,"timestamp":1784197376805}}
{"type":"message","id":"362f40b3","parentId":"3f457b97","timestamp":"2026-07-16T10:22:56.864Z","message":{"role":"toolResult","toolCallId":"call_XwrIGddcBasIrtsnLX8Fqr7c|fc_017bff223290b7ba016a58b100c18c819aab61b213da78e46a","toolName":"grep","content":[{"type":"text","text":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 2|\n*3|**Status:** **Ratified (2026-06-09).** Independently verified-with-amendments (2026-06-09, agent `doyle` — see §0); six amendments folded in (marked **[V1]**–**[V6]**). The operator ratified both open decisions: the §9 artifacts and the **sequencing** decision (§6.1 — restoration = next milestone, before `spt-claude-code`). The §9 artifacts are now **delivered** (ADR-0018, `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT`, KNOWN-HAZARDS 6.7/6.8); build plan = `RESTORATI...\n 4|\n 5|**Audience:** dual (human reviewer + AI dev-agent). This is a *why* document, not an implementation plan.\n 6|\n...\n 51|- **broker** (stable kernel) — holds *only* the un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets, and (per the §B ownership table) the Iroh/QUIC endpoint + conn table. Minimal, versioned local IPC. Almost never updates.\n*52|- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk and re-attaches to broker-held handles.\n 53|\n 54|Explicit evidence the split was meant to be a **process** boundary:\n 55|- ADR-0004 Consequences: *\"A small internal broker **process** exists beneath the daemon — a deliberate, bounded walk-back of 'literally one process,' … guaranteeing endpoint survival across updates.\"*\n*56|- ADR-0004 rejected alternatives include **\"whole-daemon live FD-passing\"** and **\"drain + restart\"** — i.e. the chosen design is specifically *restart the brain, the broker process survives.*\n 57|- `M3a-PLAN.md`, `M3-PLAN.md`, `M3b-PLAN.md` all name \"the broker **process** + IPC (M3b).\"\n 58|- Spikes 01/03/04/05/06 each **PROVEN** with *two separate binaries* (`spt-spikes/spike-01-broker-handoff`): a PTY child + a live QUIC transfer survive 100× brain restarts, gapless and exactly-once. A spike that proves \"survives a brain restart\" presupposes the broker is a separate process.\n 59|\n...\n 156|\n*157|**Decision.** The broker tracks each session-subscriber's last-delivered cursor. On (re)start the new brain queries the broker for **all** hosted sessions and re-attaches each in **resume** mode. **Output is at-least-once** (the broker resumes from last-*sent*); **input/effects stay exactly-once** via the already-broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* is **retired** for production — the new brain cold-starts and reconstructs continuity by querying the broker.\n 158|\n 159|**[V2] Generation custody.** Retiring `BrainState` would orphan the generation counter (KH 2.4: increments on every start/revive; `gen_start` always `= now()`). Custody moves to the **broker**: it owns the counter, increments it on **every** brain spawn (planned or crash), and hands `{generation, start-reason}` to the brain at spawn time via a **versioned argv/hello field** (KH-2.3 forward-compat, defaulted). Because the broker observes every respawn, this is strictly more reliable than passing it brain-...\n 160|\n...\n 162|- Two concrete gaps exist today: `applyhost.rs:239` re-attaches only `sessions.first()` (one session) and with `from_seq = 0` (re-replays the whole ring → duplicate output).\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...\n 199|\n*200|This is **milestone-sized**, not a patch. It includes: the process decomposition; an IPC-verb surface for the hard cases (digest push, seed-lock/liveness, plus the general direct-call audit — net bring-up is near-free per the §Q5 audit correction); broker-side per-session delivered-cursor tracking; broker-owned generation custody handed at spawn **[V2]**; durable absolute-deadline timing across **phase-significant** resident loops only **[V4]** (the idempotent pump cadences need *no* conversion — co...\n 201|\n 202|### 6.1 Sequencing — RATIFIED (operator, 2026-06-09)\n 203|\n...\n 206|- **(a) Queue is open** — mesh shipped at v0.3.0, M8 acceptance is COMPLETE.\n*207|- **(b) Self-bootstrapping (decisive).** Until this ships, *every* release needs manual daemon bounces fleet-wide — paid on all 3 nodes for v0.3.2, and `enlyzeam` reproduced the `\\r`-corruption for ~a day with the fix sitting on disk. The restoration release is the **last** one that needs a manual bounce; once it lands, every adapter-era release rolls seamlessly.\n*208|- **(c) Adapter window safe.** The split changes daemon *internals*, not the CLI/api surface M8 froze. Better the adapter lands on the final topology than atop a daemon we later perform open-heart surgery on while it hosts the user's daily driver.\n 209|\n 210|Ratified by the operator 2026-06-09.\n 211|\n...\n 215|\n*216|- **Alarms have no durable daemon scheduler.** In the spt-core daemon, `alarm` exists only as an event *shape* (`spt-proto`) + relay handling (`psyrelay.rs:93`) + a test fixture; `target-time` is only a wire attribute. The actual \"wait until target, fire the TIMED PULSE\" timer lives in the legacy owl listener in-memory. Porting alarms into the daemon as a durable scheduler (which would then ride the Q4 one-shot mechanism) is its own gap.\n*217|- **Registry one-cadence reconverge window.** `RegistryHost` is intentionally in-memory (`registryhost.rs:19-26`): a restart forgets peers' rows and reconverges within one pump cadence (self-rows re-advertised each tick; mirrored to `identity/registry/` JSON for out-of-process readers). Across a brain restart this is a brief reconverge window, accepted by design — not a blocker, noted for completeness.\n*218|- **Multipart inter-agent message truncation — belongs to LEGACY `claude_skill_owl`, not spt-core.** During the verification exchange (which ran over the legacy live-agent messaging infra — the `owl.exe` listener, not the spt-core daemon), an 18-part chunked message reached the recipient as ~parts 1–8; a long resend also dropped its tail. Long messages silently lose their tail — a delivery-integrity issue in the chunk-reassembly path. **This is a legacy-project bug** (the agents in this exchange ...\n 219|- **Stale DEFERRED mesh row.** Side-flag from verification: the mesh row in `docs/DEFERRED.md` is stale (mesh shipped at v0.3.0). Minor cleanup, noted for a docs pass.\n 220|\n 221|---\n...\n 228|- **Spikes** — `docs/spikes/SPIKE-01-broker-handoff.md`, `-03-quic-survival`, `-04-forkpty-parity`, `-05-restart-stress`, `-06-idempotent-boundary` (proofs that the two-process handoff works on both OSes).\n*229|- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.\n 230|\n 231|---\n 232|\n*233|## 9. Artifacts — DELIVERED (ratified 2026-06-09)\n 234|\n 235|Verification is done (§0); the operator ratified; these durable artifacts are now written (commit 44497f4 unless noted):\n 236|\n 237|- **ADR-0018** (`docs/adr/0018-broker-brain-process-isolation-restoration.md`) — extends + amends ADR-0004: the drift finding, the Q2–Q8 restoration decisions, the six verification amendments. Status: Accepted (2026-06-09).\n*238|- **`REQ-*` registry mints** (registry-first, `traceable-reqs.toml`; minted INACTIVE per rule 5, activated per `RESTORATION-PLAN.md`'s schedule):\n 239|  - **`REQ-HAZARD-BROKER-PROCESS-ISOLATION`** — a brain restart must never drop a hosted endpoint (REQ-UPD-3's real promise). **Evidence [V5]:** an `int` stage productionizing SPIKE-01/03 — a PTY child + a live QUIC conn survive a brain-*process* restart onto a *swapped* binary. REQ-UPD-3 / REQ-DAEMON-2's current `int` evidence (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`) proves only the *in-process* handoff shape — **mis-evidenced today, re-pointed at plan task D7.**\n 240|  - **`REQ-HAZARD-ROLLBACK-STATE-COMPAT` [V1]** — a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable).\n 241|  - The functional two-process-split realization + the **[V6]** N-1 old-broker × new-brain CI compat test are carried as `RESTORATION-PLAN.md` tasks D1–D7 (the compat test lands as a D7 gate).\n## CONTEXT-MEMORY.md\n 40|\n*41|The live/project authoring directives are **core-owned** — fed to every Psyche by spt-core (part of the composed psyche-download the daemon writes to the turn's `{psyche_context_file}`), so the split is consistent across all adapters (inconsistent rules → sync chaos). Adapters add only harness mechanics (how to write the drop file).\n 42|\n 43|## memformat (deeply integrated, self-evolving, the split scaffold)\n 44|\n...\n 69|\n*70|`Concepts`/`Aliases` make context **retrievable**; `Applies to` enables fine-grained scoping + query-routing; `Source` carries provenance; `Pinned` drives start-injection density (below).\n 71|\n 72|## Retrieval Map (generated INDEX)\n 73|\n*74|A generated, lightweight **INDEX** over topics — summaries + concepts + aliases + paths — **not** a vector DB, **not** a full mirror (adapted from pi-agent-memory's Retrieval Map). One INDEX per **project** (aggregating all agents' project context → realizes cross-agent synthesis + query-routing: \"which endpoint owns this task\") and per **agent**. Regenerated on context change; the merge driver + INDEX regeneration keep it current.\n 75|\n 76|## Start-injection: dense working set, sparse tail\n 77|\n## DEBUG-CONVERGE-PLAN.md\n 32|  convergence table, exit codes, loopback + two-host tests.\n*33|- **Out (still deferred):** remote build orchestration, dashboards/TUI, adapter\n 34|  inclusion, stable-release convergence. Keep the first watcher narrow.\n 35|\n 36|## CLI\n...\n 53|trust-derived subnet membership introduced in `b9326f4` (self + pinned peers,\n*54|registry rows as enrichment) so a freshly-joined or eviction-decayed node is\n 55|still *expected* and shows up as a row, not a silent omission. `--nodes` gives\n 56|an explicit set for targeted tests.\n 57|\n...\n 87|   the responder's identity for free.\n*88|2. **Registry/presence enrichment.** If a node already publishes its applied\n*89|   version + provenance into a registry row (the rollout metadata carries\n 90|   `git_commit`/version provenance), the coordinator can read convergence from\n 91|   rows it already syncs — no new round-trip. Verify whether\n*92|   `relcache`/registry persists \"applied version\" anywhere queryable; if not,\n 93|   prefer option 1.\n 94|\n 95|Pick option 1 as the baseline (self-contained, testable on loopback); treat\n## DEFERRED.md\n 7|| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any \"scroll back further than the buffer\" user need |\n*8|| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |\n 9|| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |\n 10|| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |\n*11|| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one...\n 12|| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |\n 13|| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant s...\n 14|| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |\n*15|| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |\n*16|| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |\n 17|| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |\n*18|| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |\n 19|| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the ...\n 20|| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concr...\n*21|| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect,...\n 22|| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-u...\n 23|| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the \"no connection\" state in `subnet status` + the coming-online banner...\n 24|| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8...\n*25|| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruni...\n*26|| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n*27|| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite ho...\n 28|| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\n 29|| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache...\n 30|| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n*31|| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`w...\n*32|| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version ...\n 33|| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n 34|| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer \"serving subnet X?\", a...\n 35|| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures...\n...\n 40|| **On-disk `current_exe` reconcile after auto-rollback (the D6-2 reboot residual)**: readiness-gated auto-rollback (ADR-0018 Q7, restoration D6-2) is **record-driven selection** — after a rollback the broker's supervisor keeps spawning the last-known-good `.old-N` binary, chosen from the durable `RolledBack` record, so the rescue survives a reboot for free **with no file rename at the failure instant** (the brittle path the design rejected). But the bytes on disk at `current_exe` are STILL the quarant...\n*41|| **Quarantine operator override (`spt update apply --force` — the note-i escape hatch)**: D6-2's quarantine guard refuses re-applying a version that auto-rolled-back on this node (the `RolledBack` record is the marker; clears naturally when N+1 stages). But a version whose failure was **environmental** (a transient boot condition, not bad bytes) leaves the node **permanently allergic** to a genuinely-good version with no escape but staging a newer one. FIX = an explicit operator override (`spt update ...\n 42|| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers m...\n*43|| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's \"remind me at 3pm\" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today...\n 44|| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **aut...\n*45|| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n 46|| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47|| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n*48|| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n*49|| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n*50|| **Always-on endpoint kind + `#`-sigil addressing (ADR-0023; REQ-EP-8 / REQ-EP-9) — the core seam for adapter-bridged group/forum messaging**: a `[always-on]` manifest section declaring a resident, owner-less, addressable endpoint binary the daemon boot-launches + supervises **continuously** (independent of agent liveness, register-triggered, one per `<adapter>[:profile]`), which self-manages its `#`-addressed channel endpoints via the existing `api bind` and may call the already-ungated `endpoint wak...\n## DOCS-STRATEGY.md\n 30|- **Human dev:** two agents exchange a message end-to-end in **one command + minimal config**, < 10 min — the core primitive, whole-thing-first, then decomposed.\n*31|- **Dev-agent integrator:** a minimal **adapter** that satisfies the manifest + `api` contract and drives one lifecycle operation — the \"build a harness for spt-core\" hello-world.\n 32|\n 33|## Agent-consumable docs (spt-core specifics)\n 34|\n...\n 38|- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry the canonical path and drop human-only narrative.\n*39|- **Machine-readable manifest/contract schema** (JSON Schema) at a stable, discoverable path — agents validate their adapter manifests against it; the schema *is* documentation.\n 40|- **MCP doc/resource server** — the standout fit, given the harness contract is the core integration surface. Expose the contract + reference as **deterministic, side-effect-free MCP resources** (same request → same result), giving integrating agents structured knowledge without RAG fuzziness. Highest-leverage agent-docs move for spt-core.\n 41|- **CLI help as first-class agent docs** — agents primarily shell `spt api …`, so `spt <cmd> --help` must be complete, exampled, and machine-readable (a `--json`/structured help mode). The CLI surface is a documentation surface.\n 42|\n...\n 44|\n*45|Deployed to GitHub Pages on `spt-releases` (ADR-0014; truth + CI generation stay in the source repo). **Tier 1 (ships with v0.1):** mental model/overview · human quickstart (install → two agents message) · adapter quickstart (via `mock-adapter`) · **harness contract vertical complete** (public manifest reference + full `spt api` reference + install-on-demand bootstrap pattern) · shell adapter getting-started (`spt-shell-notify` as the live example) · CLI reference generated from clap, CI-gated · ...\n 46|\n 47|**Site generator: mdBook + custom theme CSS** (Rust-toolchain-coherent — one `cargo install mdbook` on the existing runners; the Rust Book IA this strategy copies *is* mdBook). Styling north star: **Astro Starlight** — copy its look/feel in the theme CSS, not its toolchain. Raw `.md` published alongside rendered pages (`/x.html` ↔ `/x.md`) for the agent-export convention; `llms.txt` / `llms-full.txt` / `manifest.schema.json` as static assets at site root.\n 48|\n...\n 82|- [Quickstart: two agents exchange a message](…/quickstart/messaging.md): one command, <10 min.\n*83|- [Quickstart: build an adapter](…/quickstart/adapter.md): minimal manifest + `api` contract.\n 84|- [Mental model](…/concepts/overview.md): daemon, endpoints, instances, subnet.\n 85|\n 86|## Capabilities      (one bullet per vertical, each → its own llms.txt)\n...\n 101|# Quickstart — two agents exchange a message, end to end, <10 min.\n*102|> Pick your path: **developer** (here) · **building an adapter / dev-agent** → /quickstart/adapter.md\n 103|\n 104|## 1. Install (one line)        curl -fsSL https://spt-core.dev/install.sh | sh\n 105|   Verify:                      spt --version      # spt 0.1.0 (proto v1)\n...\n 119|0 Getting started → 1 Quickstart (the early runnable project) → 2 Mental model\n*120|→ 3 Capability verticals (build-order) → 4 Capstone: build a non-CC adapter end-to-end\n 121|```\n 122|**Vertical ordering (dependency-sequenced):**\n 123|`Messaging → Lifecycle → Terminal → Networking → Harness contract → Instances → Shells → Self-update`\n...\n 140|/// The manifest's `min_spt_core_version` exceeds this binary.\n*141|/// **Fix:** update spt-core, or pin the adapter to a compatible release.\n 142|VersionTooOld { required: Version, found: Version },\n 143|```\n 144|\n## DORMANCY-BUDGET.md\n 5|**What this answers.** ADR-0003's Stage-A red-team #9: *\"dormant-warm default\n*6|has real resource/security cost — measure N dormant sessions on real adapters;\n*7|define an adapter-specific warm/cold policy + resource budget before locking\n 8|warm as the default.\"* This document records the measurement and locks the\n 9|policy with numbers instead of a guess.\n 10|\n...\n 36|Knobs: `SPT_BUDGET_N` (seats), `SPT_BUDGET_PROGRAM`/`SPT_BUDGET_ARGS` (the\n*37|seat command — point at the real adapter's harness), `SPT_BUDGET_SETTLE_MS`,\n 38|`SPT_BUDGET_IDLE_MS`. Rig runs ride the CI `[budget]` commit-message gate\n 39|(both hosts measure in one tagged push) or a direct local\n 40|`SPT_BUDGET=1 cargo test -p spt-daemon --test budget --release -- --nocapture`.\n...\n 53|\n*54|3 × `claude` (real adapter seat — the heavy case):\n 55|\n 56|| metric | per-seat |\n 57||---|---|\n...\n 69|5 × `sh` (shell seat — no LLM harness installed on this host, so the shell\n*70|floor is its measurement; the adapter-class number generalizes from\n 71|HFENDULEAM's, the harness binary being the same node runtime there):\n 72|\n 73|| metric | per-seat |\n...\n 84|1. **Warm (dormant) stays the default.** An idle warm seat burns **zero CPU**\n*85|   on both adapter classes; the cost is purely resident memory. A shell-class\n 86|   seat is ~8 MiB — effectively free at any realistic seat count. A heavy\n 87|   LLM-harness seat is ~300 MiB — a workstation-class node holds ten warm\n 88|   seats in ~3 GB, acceptable for the instant-reactivation win\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n 10|seam) are spt-core changes** (todlando's wave, sequenced after F-029). **The\n*11|trust-seeder binary itself is adapter-side** (perri's wave — spt-core stays\n*12|harness-agnostic per the glue-model boundary: adapters = manifest + binary only;\n 13|no harness-aware source in spt-core).\n 14|\n 15|---\n...\n 43|| 2 | ACCOUNT-ROOT-MISMATCH — trust seeded in a DIFFERENT config root than the spawn resolves; wedges despite a valid-looking seed | Reavo/Brandon dual-root box (si1 vs b6b probes) | SILENT |\n*44|| 3 | PSYCHE-CRASHLOOP — nested psyche CC dies at trust dialog; wrapper respawned ~3/s, ordinal 5358 in 30 min | F-h; now guarded by adapter v0.13.0 give-up + core C3(b) | SILENT (guarded) |\n*45|| 4 | ADAPTER-UNRESOLVED — no registered adapter hosts the parent binary | seed→listen path, bash parent | LOUD pre-spawn (good shape) |\n 46|| 5 | STALE-CLI-CONTRACT — bringup recipe passes a removed flag; \"unexpected argument\"; spawn never happens | old recipe on 0.22.0+ | LOUD pre-spawn (good shape) |\n 47|| 6 | RELAUNCH-NEVER-BINDS — 2nd `endpoint run` same id after tree-kill; prints but never binds | **0.22.0 only — FIXED on 0.23.0 by F-028 B1/B4** (perri definitive repro 2026-07-03: tree-kill → same-id re-run re-binds fresh session, live PTY, honest info.json; first check was rig-void, --subnet omitted, caught before reporting) | FIXED-NOTE (not in F-027 scope) |\n 48|| — | UAC-740 elevation spawn-fail | NOT observed (subnet-op elevation only) | out of scope |\n...\n 93|  deadline separates cleanly at ≥30 s; **never configure below 30 s**),\n*94|  overridable per-adapter via a manifest key (the bringup-time contract\n*95|  belongs to the adapter) and per-invocation via `--bind-deadline <secs>`\n 96|  (the per-node escape for cold-broker first-spawn / loaded boxes). Escape\n 97|  hatch `--no-wait` restores fire-and-forget and prints\n 98|  `ENDPOINT_RUN_DETACHED:{id}` (explicitly unverified — never `_STARTED`).\n...\n 105|\n*106|### Half B — pre-spawn manifest seam (spt-core executes; adapter owns the binary)\n 107|\n 108|New manifest key **`pre_spawn`** inside `[session.<role>]` tables (applies to\n 109|`self` AND `resume` — field-confirmed necessary on resume: a resume is a\n...\n 115|1. **Env parity via capture-at-creation (BINDING; sharpened by perri review):**\n*116|   env vars the adapter declares `[env.<NAME>] direction=\"read\"` are **captured\n 117|   at endpoint creation and stamped into the perch record**; the broker threads\n 118|   the IDENTICAL stamped values to BOTH the pre-spawn command AND the session\n 119|   spawn. Neither consumer reads its own process env for a stamped var — if the\n...\n 126|   `[env]` declares, knowing nothing of `CLAUDE_CONFIG_DIR` specifically.\n*127|   (Field-confirmed adapter shape: claude-spt declares\n 128|   `[env.CLAUDE_CONFIG_DIR] direction=\"read\"` with `~/.claude` fallback —\n*129|   adapter reads, never sets.)\n 130|2. **Install-dir-first resolution (REQ-INSTALL-11):** the program path resolves\n*131|   via `resolve_program_in_dir` like every other adapter-manifest program.\n 132|3. **Bounded + loud:** runtime capped (30 s); non-zero exit or timeout ABORTS\n 133|   the session spawn with `ENDPOINT_SPAWN_FAIL:{id}: reason=PRE_SPAWN_FAIL:\n 134|   <exit/detail>` on CLI stderr (relayed through the spawn RPC error — this\n...\n 163|  owlery cwd is trust-seeded by the same pre-spawn seam, and the crashloop\n*164|  backstop (C3(b) + adapter give-up) already guards the symptom.\n 165|- No mode-6 work: FIXED on 0.23.0 by F-028 B1/B4 (perri definitive repro).\n 166|  The Half-A deadline token still catches its shape if it ever regresses.\n 167|\n...\n 174|| 3 PSYCHE-CRASHLOOP | Half B (owlery cwd seed) | existing C3(b)/v0.13.0 guard |\n*175|| 4 ADAPTER-UNRESOLVED | — (already good) | existing pre-spawn error |\n 176|| 5 STALE-CLI-CONTRACT | — (already good) | existing / Half A `EXITED_PRE_BIND` |\n 177|| 6 RELAUNCH-NEVER-BINDS | FIXED (F-028 B1/B4, 0.23.0-verified) | Half A `NO_SESSION`/`WEDGED_PRE_BIND` (regression net only) |\n 178|\n...\n 184|  (never binds) → assert today's silent `ENDPOINT_RUN_STARTED` becomes the\n*185|  token. Real-daemon int candidate (mock-adapter seam exists — F-028 pattern).\n 186|- `REQ-PRESPAWN-COMMAND-SEAM` — manifest `pre_spawn` executes with spawn-env\n 187|  parity + install-dir-first resolution + bounded runtime; failure aborts\n 188|  loud. Red-first unit on env parity (seeded sentinel var visible to pre-spawn\n...\n 191|  bug, perri 2026-07-03): the conformance test IS the Half-A red-first int.\n*192|- Adapter-side (perri's repo, NOT a core REQ): trust-seeder contract —\n 193|  forward-slash exact key, root-as-spawn-resolves, idempotent.\n 194|\n 195|## 6. Docs surfaces (docs-drift gate applies)\n...\n 218|   the session spawn consume the stamp — neither reads its own process env.\n*219|   Adapter shape confirmed: `[env.CLAUDE_CONFIG_DIR] direction=\"read\"`,\n*220|   fallback `~/.claude`, adapter reads and never sets.\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n 3|Status: **READY FOR BUILD 2026-07-04 — wave called by operator.** Author: doyle.\n*4|Inputs: perri `PSYCHE-EPHEMERAL-PLAN.md` (adapter half, doyle-gated PASS; built on\n 5|parked branch `psyche-ephemeral-parked`, publish held for this core wave), the\n 6|2026-07-04 field case (both directions — v0.13.2 bad-ship + hall-bf multi-subnet\n 7|poll refusal, evidence table §2), doyle code recon (anchors verified @f8428a8).\n...\n 10|spt-core** (todlando's wave). **The psyche role binaries/templates are\n*11|adapter-side** (perri's parked branch — publishes ONLY after the core floor\n 12|releases; see §7 sequencing, a BINDING lesson from the v0.13.2 bad ship).\n 13|\n 14|---\n...\n 35|\n*36|**(a) The adapter cannot HOLD residency.** The resident runner's per-pulse\n 37|liveness poll (`spt ready <id> --once`) hard-refuses on a multi-subnet home\n 38|(`READY_FAIL: … pass --subnet`). The nested psyche process **cannot know its\n 39|subnet/home**: core fills only `base_keys` = `{id}`, `{session_id}`, `{node}`\n...\n 42|the daemon re-hosted, ~5 s churn forever: hall-bf-psyche ordinal 6491+, at\n*43|one point 54 stamped host attempts. Adapter v0.13.4 made the death LOUD\n 44|(`PSYCHE_POLL_FAIL`), but residency remains structurally impossible there.\n 45|\n 46|**(b) The daemon EXPECTS residency, and that expectation is destructive.**\n*47|Adapter v0.13.2 accidentally shipped the parked ephemeral shim: its by-design\n 48|exit after the seed turn tripped the residency machinery, which tore down the\n 49|endpoint's hosted state — ready marker removed, never re-stamped, every\n 50|force-native gated `leg=cli-gate-not-hosted` **permanently** (field brick,\n...\n 58|- **`.live-bin` own-copy:** exists solely so the long-lived resident's image\n*59|  doesn't Windows-image-lock the adapter registry dir during `spt adapter\n 60|  update` (the crc-swap `.old`-litter hazard class). Ephemeral turns hold the\n 61|  image only for a bounded turn — the own-copy machinery loses its reason.\n 62|- Psyche TURNS already work daemon-driven today (`psyche_download`, revival\n...\n 68||---|----------|-------|-----------|\n*69|| 1 | `LIVEHOST_PSYCHE_NONRESIDENT` xN in EVERY iso-home run, both adapter versions | perri RC + wakefin rigs | daemon expects residency |\n*70|| 2 | hall-bf-psyche boot-churn ~5 s cadence, ordinal 6491+, synthetic `sess-<pid>-<ts>` ids, no real session ever binds; starts exactly at BIGNET join (7/3) | operator box, real home | adapter can't hold residency |\n*71|| 3 | Poll-verb root repro: `spt ready <id> --once` on 2-subnet home → exit 1, `READY_FAIL … pass --subnet`, empty stdout | perri, clean repro | adapter can't hold residency |\n 72|| 4 | v0.13.2 shim-exit → residency teardown → ready removed → permanent `leg=cli-gate-not-hosted` | hall-bf + perri wake-confirm | residency expectation destructive |\n 73|| 5 | Psyche turns land fine via daemon-driven paths while the resident churns | hall-bf digest (`psyche_download` at every boundary) | per-event shape already proven |\n 74|| 6 | ~12/min re-host cadence NEVER tripped the C3(b) thrash guard (boot records are apparently not ledger boundaries to the guard) | hall-bf | guard blindness (rides this wave) |\n...\n 160|\n*161|Remove the own-copy spawn path for psyche roles (spawn from the registry\n*162|install dir like every other bounded adapter invocation — image-lock exposure\n 163|is now turn-bounded, same class as `translate`/`digest` today). GC existing\n 164|`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is\n 165|image-locked at update time). Compat: a manifest that still names the\n...\n 185|\n*186|- No adapter-aware code in core: core wires WHEN turns run and WHAT keys they\n*187|  get; the adapter's parked branch owns what a psyche turn does (glue-model\n 188|  boundary holds).\n 189|- No relay-unregister fix (the post-clear `owl send → NO_PERCH` casualty) —\n 190|  separate seed, C-2 follow-up (a), field-repro'd 2026-07-04, sequenced with\n...\n 205|| Thrash-guard blindness (12/min invisible) | W3 ledger-counted budget | red-first synthetic loop trips it |\n*206|| `.live-bin` image-lock class | W5 (bounded turns from registry dir) | existing crc-swap displace+GC net |\n 207|| Reseed loop (custody lost repeatedly) | W2 reseed + W1 budget | `PSYCHE_RESEED` marker + budget stamp |\n 208|\n 209|## 5. Requirement seeds (register at wave start, `required_stages=[]` until the chunk activates)\n...\n 216|  survives the turn.\n*217|- `REQ-PSYCHE-SID-CUSTODY` — psyche templates receive the psyche's OWN sid;\n 218|  parent boundary does not rotate it; reseed is loud. Red-first: parent\n 219|  `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's\n 220|  — guard-revert reproduces).\n...\n 227|  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,\n*228|  failing psyche → parent stays deliverable, no rehost churn, error stamped.\n 229|- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the\n 230|  failure budget counts real attempts (ledger-derived); a 12/min failure loop\n 231|  trips it. Red-first synthetic loop.\n*232|- Adapter-side (perri's repo, NOT core REQs): parked-branch ephemeral roles;\n 233|  `min_spt_core` floor = the release that ships this wave; publish gated on\n 234|  that floor being LIVE (bad-ship lesson, binding).\n 235|\n...\n 244|  **single-node multi-subnet rig** (the hall-bf shape: 2 subnets, one home).\n*245|- **Adapter (perri):** `psyche-ephemeral-parked` branch rebased once core API\n 246|  shape freezes (she reviews W1/W2 key contract before her rebase — field\n 247|  review slot like F-027 §7); publish ONLY after core floor releases.\n 248|- **hall-bf interim:** stays turn-functional + visibly stamped (v0.13.4) until\n## F-035-CONFLICT.md\n 3|**Raised by:** todlando, mid-build, 2026-07-09. **Build HELD** (WIP uncommitted on\n*4|`f-035-window-drain-honor`). doyle's COLLAPSE ruling collides with a **shipped**\n 5|acceptance gate my RCA (docs/F-035-RCA.md) missed. Need an A/B/C ruling before I proceed.\n 6|\n 7|---\n...\n 13|\n*14|**`crates/spt/tests/idle_edge_drain_e2e.rs:89`** — `fn spool_while_active_then_idle_fires_injection`\n*15|`// [int->REQ-MSG-IDLE-EDGE-DRAIN] [int->REQ-HAZARD-DELIVERY-STARVATION]`\n 16|\n*17|- **Setup:** spools `spool_message_deferred_at(perch, \"peer\", …)` — which is\n*18|  `insert_message(…, WINDOW_ACTIVE_ONLY, …)` (spool.rs:363). So the row is\n 19|  **`active_only`**.\n 20|- **Trigger:** `spt api state idle <id>` — the real ACTIVE→IDLE edge.\n 21|- **Assertion (the keystone):** `assert_eq!(pending_after, 0, …)` — the `active_only`\n*22|  row **MUST be delivered** on the idle edge (spool cleared).\n 23|\n*24|My fix → the `active_only` row is excluded from the idle claim → stays spooled →\n 25|`pending_after == 1` → **this shipped gate goes RED.**\n 26|\n*27|`REQ-MSG-IDLE-EDGE-DRAIN` (F-023 leg 2) and `REQ-SEND-WINDOW-DRAIN-HONOR` (F-035) are\n 28|directly incompatible for the relay-less spt-hosted class. The active/idle asymmetry I\n 29|called a bug in the RCA is **intentional** for that class.\n 30|\n...\n 35|For an **spt-hosted** endpoint (broker owns the PTY, `controllable`, relay-less), the\n*36|idle-edge/parked drain is the **ONLY** carrier for an `active_only` row. Traced every path:\n 37|\n 38|| carrier | `active_only` to spt-hosted? |\n 39||---|---|\n*40|| Send-time inject (`cli.rs:5716` `try_inject = prefer_native \\|\\| !active_only`) | **NO** — cli.rs:5713 verbatim: *\"a non-native `--active-only` is the explicit hook channel: **spool-only, no inject attempt**.\"* |\n 41|| Send-time idle re-offer (`cli.rs:5734`) | **NO** — gated `&& !active_only`. |\n*42|| Hook poll `drain_active_window` (`api poll`) | **N/A** — that is the HARNESS-hosted hook channel; an spt-hosted endpoint has no `api poll` harness. |\n*43|| **Idle-edge / parked drain `drain_idle_spool`** | **YES** — the F-023 leg-2 behavior (`include_deferred = !resting`). **This is the only carrier.** |\n 44|\n 45|So: `active_only` is designed as *\"the active **hook** window\"* — meant for\n 46|**harness-hosted** endpoints that `api poll`. An **spt-hosted** endpoint has no hook, so\n...\n 59|  `active_only`+spt-hosted is knowingly revoked (that class starves, or gets an\n*60|  evaporate/refuse-at-send policy instead). Requires re-scoping `REQ-MSG-IDLE-EDGE-DRAIN`\n 61|  + rewriting its int.\n 62|- **(B) never-starve wins** — the idle edge *should* carry `active_only` to spt-hosted\n 63|  (F-023 correct). Then **F-035 is mis-scoped**: *\"never wakes\"* is the **harness** hook\n...\n 65|  never a contract for the hook-less spt-hosted class. **RETIRE or NARROW\n*66|  REQ-SEND-WINDOW-DRAIN-HONOR; no code change.**\n*67|- **(C) class split** — spt-hosted delivers `active_only` at idle **without waking**\n 68|  (non-interrupting background inject) vs harness-hosted holds-for-poll. New behavior,\n*69|  larger; needs a \"deliver-but-don't-wake\" inject mode the translation binary honors.\n 70|\n 71|**todlando lean:** the carrier table points at **(B)** — the field-doc *\"never wakes an\n 72|idle target\"* (cli.rs:85) describes the harness hook model, and F-023 already reasoned\n...\n 74|`--deferred` background row reaching an idle spt-hosted agent) may be **working as\n*75|designed** — it is the only way that class can ever be delivered. If the true concern is\n*76|*\"it interrupts,\"* that is **(C)**, not the drain-exclusion of **(A)**. But this is your call.\n 77|\n*78|My WIP (spool.rs window-axis collapse + inject.rs + 2 units) is staged **uncommitted** — I\n*79|touched neither delivery.rs nor worker.rs. Standing by for A / B / C.\n## F-035-RCA.md\n*1|<!-- [doc->REQ-SEND-WINDOW-DRAIN-HONOR] F-035 RCA — report-before-fix. -->\n*2|# F-035 RCA — idle-edge parked-drain ignores the delivery-window tag\n 3|\n 4|**Status:** RCA-first, **report before fixing** (doyle-dispatched 2026-07-09).\n*5|**REQ:** `REQ-SEND-WINDOW-DRAIN-HONOR` (registered, `required_stages = []` — activate on doyle's fix ruling).\n 6|**Investigator:** todlando. **Off:** `origin/main@2c05dc9` (v0.30.2).\n 7|\n 8|---\n...\n 20|\n*21|For an **spt-hosted, relay-less** endpoint (the class the idle-edge drain targets),\n*22|such a row is delivered through the translation binary at the idle window — the\n*23|exact delivery the tag forbids.\n 24|\n 25|---\n 26|\n...\n 28|\n*29|The two delivery-window surfaces are **asymmetric**. One honors the `window` axis;\n 30|the other keys on the legacy `deferred` mirror and **never consults `window`**.\n 31|\n*32|### The delivery-window model (ADR-0028 W2, `spt-store/src/spool.rs`)\n 33|\n 34|| window        | active hook poll | idle / wake |\n\n[Showing lines 1-458 of 853 (50.0KB limit). Use :459 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"src, tests, docs, CONTEXT.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":242,"fileCount":20,"files":["docs/BROKER-BRAIN-SPLIT-RESTORATION.md","docs/CONTEXT-MEMORY.md","docs/DEBUG-CONVERGE-PLAN.md","docs/DEFERRED.md","docs/DOCS-STRATEGY.md","docs/DORMANCY-BUDGET.md","docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md","docs/F-035-CONFLICT.md","docs/F-035-RCA.md","docs/F-035-RELAY-RCA.md","docs/FAULT-MATRIX.md","docs/FLAKE-LEDGER.md","docs/GATEWAY-LIVENESS-DISPATCH.md","docs/GATEWAY-RCA-STEP1.md","docs/INJECT-SETTLE-REARM-GATE-VERDICT.md","docs/KNOWN-HAZARDS.md","docs/MANIFEST.md","docs/MSG-IDENTITY-DISPATCH.md","docs/MUSL-RELEASE-HANDOFF.md"],"fileMatches":[{"path":"docs/BROKER-BRAIN-SPLIT-RESTORATION.md","count":14},{"path":"docs/CONTEXT-MEMORY.md","count":3},{"path":"docs/DEBUG-CONVERGE-PLAN.md","count":5},{"path":"docs/DEFERRED.md","count":17},{"path":"docs/DOCS-STRATEGY.md","count":7},{"path":"docs/DORMANCY-BUDGET.md","count":6},{"path":"docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","count":17},{"path":"docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md","count":18},{"path":"docs/F-035-CONFLICT.md","count":20},{"path":"docs/F-035-RCA.md","count":20},{"path":"docs/F-035-RELAY-RCA.md","count":18},{"path":"docs/FAULT-MATRIX.md","count":18},{"path":"docs/FLAKE-LEDGER.md","count":4},{"path":"docs/GATEWAY-LIVENESS-DISPATCH.md","count":8},{"path":"docs/GATEWAY-RCA-STEP1.md","count":9},{"path":"docs/INJECT-SETTLE-REARM-GATE-VERDICT.md","count":4},{"path":"docs/KNOWN-HAZARDS.md","count":20},{"path":"docs/MANIFEST.md","count":20},{"path":"docs/MSG-IDENTITY-DISPATCH.md","count":13},{"path":"docs/MUSL-RELEASE-HANDOFF.md","count":1}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n   2│\n  *3│**Status:** **Ratified (2026-06-09).** Independently verified-with-amendments (2026-06-09, agent `doyle` — see §0); six amendments folded in (marked **[V1]**–**[V6]**). The operator ratified both open decisions: the §9 artifacts and the **sequencing** decision (§6.1 — restoration = next milestone, before `spt-claude-code`). The §9 artifacts are now **delivered** (ADR-0018, `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT`, KNOWN-HAZARDS 6.7/6.8); build plan = `RESTORATI...\n   4│\n   5│**Audience:** dual (human reviewer + AI dev-agent). This is a *why* document, not an implementation plan.\n   6│\n    │...\n  51│- **broker** (stable kernel) — holds *only* the un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets, and (per the §B ownership table) the Iroh/QUIC endpoint + conn table. Minimal, versioned local IPC. Almost never updates.\n *52│- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk and re-attaches to broker-held handles.\n  53│\n  54│Explicit evidence the split was meant to be a **process** boundary:\n  55│- ADR-0004 Consequences: *\"A small internal broker **process** exists beneath the daemon — a deliberate, bounded walk-back of 'literally one process,' … guaranteeing endpoint survival across updates.\"*\n *56│- ADR-0004 rejected alternatives include **\"whole-daemon live FD-passing\"** and **\"drain + restart\"** — i.e. the chosen design is specifically *restart the brain, the broker process survives.*\n  57│- `M3a-PLAN.md`, `M3-PLAN.md`, `M3b-PLAN.md` all name \"the broker **process** + IPC (M3b).\"\n  58│- Spikes 01/03/04/05/06 each **PROVEN** with *two separate binaries* (`spt-spikes/spike-01-broker-handoff`): a PTY child + a live QUIC transfer survive 100× brain restarts, gapless and exactly-once. A spike that proves \"survives a brain restart\" presupposes the broker is a separate process.\n  59│\n    │...\n 156│\n*157│**Decision.** The broker tracks each session-subscriber's last-delivered cursor. On (re)start the new brain queries the broker for **all** hosted sessions and re-attaches each in **resume** mode. **Output is at-least-once** (the broker resumes from last-*sent*); **input/effects stay exactly-once** via the already-broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* is **retired** for production — the new brain cold-starts and reconstructs continuity by querying the broker.\n 158│\n 159│**[V2] Generation custody.** Retiring `BrainState` would orphan the generation counter (KH 2.4: increments on every start/revive; `gen_start` always `= now()`). Custody moves to the **broker**: it owns the counter, increments it on **every** brain spawn (planned or crash), and hands `{generation, start-reason}` to the brain at spawn time via a **versioned argv/hello field** (KH-2.3 forward-compat, defaulted). Because the broker observes every respawn, this is strictly more reliable than passing it brain-...\n 160│\n    │...\n 162│- Two concrete gaps exist today: `applyhost.rs:239` re-attaches only `sessions.first()` (one session) and with `from_seq = 0` (re-replays the whole ring → duplicate output).\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    │...\n 199│\n*200│This is **milestone-sized**, not a patch. It includes: the process decomposition; an IPC-verb surface for the hard cases (digest push, seed-lock/liveness, plus the general direct-call audit — net bring-up is near-free per the §Q5 audit correction); broker-side per-session delivered-cursor tracking; broker-owned generation custody handed at spawn **[V2]**; durable absolute-deadline timing across **phase-significant** resident loops only **[V4]** (the idempotent pump cadences need *no* conversion — co...\n 201│\n 202│### 6.1 Sequencing — RATIFIED (operator, 2026-06-09)\n 203│\n    │...\n 206│- **(a) Queue is open** — mesh shipped at v0.3.0, M8 acceptance is COMPLETE.\n*207│- **(b) Self-bootstrapping (decisive).** Until this ships, *every* release needs manual daemon bounces fleet-wide — paid on all 3 nodes for v0.3.2, and `enlyzeam` reproduced the `\\r`-corruption for ~a day with the fix sitting on disk. The restoration release is the **last** one that needs a manual bounce; once it lands, every adapter-era release rolls seamlessly.\n*208│- **(c) Adapter window safe.** The split changes daemon *internals*, not the CLI/api surface M8 froze. Better the adapter lands on the final topology than atop a daemon we later perform open-heart surgery on while it hosts the user's daily driver.\n 209│\n 210│Ratified by the operator 2026-06-09.\n 211│\n    │...\n 215│\n*216│- **Alarms have no durable daemon scheduler.** In the spt-core daemon, `alarm` exists only as an event *shape* (`spt-proto`) + relay handling (`psyrelay.rs:93`) + a test fixture; `target-time` is only a wire attribute. The actual \"wait until target, fire the TIMED PULSE\" timer lives in the legacy owl listener in-memory. Porting alarms into the daemon as a durable scheduler (which would then ride the Q4 one-shot mechanism) is its own gap.\n*217│- **Registry one-cadence reconverge window.** `RegistryHost` is intentionally in-memory (`registryhost.rs:19-26`): a restart forgets peers' rows and reconverges within one pump cadence (self-rows re-advertised each tick; mirrored to `identity/registry/` JSON for out-of-process readers). Across a brain restart this is a brief reconverge window, accepted by design — not a blocker, noted for completeness.\n*218│- **Multipart inter-agent message truncation — belongs to LEGACY `claude_skill_owl`, not spt-core.** During the verification exchange (which ran over the legacy live-agent messaging infra — the `owl.exe` listener, not the spt-core daemon), an 18-part chunked message reached the recipient as ~parts 1–8; a long resend also dropped its tail. Long messages silently lose their tail — a delivery-integrity issue in the chunk-reassembly path. **This is a legacy-project bug** (the agents in this exchange ...\n 219│- **Stale DEFERRED mesh row.** Side-flag from verification: the mesh row in `docs/DEFERRED.md` is stale (mesh shipped at v0.3.0). Minor cleanup, noted for a docs pass.\n 220│\n 221│---\n    │...\n 228│- **Spikes** — `docs/spikes/SPIKE-01-broker-handoff.md`, `-03-quic-survival`, `-04-forkpty-parity`, `-05-restart-stress`, `-06-idempotent-boundary` (proofs that the two-process handoff works on both OSes).\n*229│- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.\n 230│\n 231│---\n 232│\n*233│## 9. Artifacts — DELIVERED (ratified 2026-06-09)\n 234│\n 235│Verification is done (§0); the operator ratified; these durable artifacts are now written (commit 44497f4 unless noted):\n 236│\n 237│- **ADR-0018** (`docs/adr/0018-broker-brain-process-isolation-restoration.md`) — extends + amends ADR-0004: the drift finding, the Q2–Q8 restoration decisions, the six verification amendments. Status: Accepted (2026-06-09).\n*238│- **`REQ-*` registry mints** (registry-first, `traceable-reqs.toml`; minted INACTIVE per rule 5, activated per `RESTORATION-PLAN.md`'s schedule):\n 239│  - **`REQ-HAZARD-BROKER-PROCESS-ISOLATION`** — a brain restart must never drop a hosted endpoint (REQ-UPD-3's real promise). **Evidence [V5]:** an `int` stage productionizing SPIKE-01/03 — a PTY child + a live QUIC conn survive a brain-*process* restart onto a *swapped* binary. REQ-UPD-3 / REQ-DAEMON-2's current `int` evidence (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`) proves only the *in-process* handoff shape — **mis-evidenced today, re-pointed at plan task D7.**\n 240│  - **`REQ-HAZARD-ROLLBACK-STATE-COMPAT` [V1]** — a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable).\n 241│  - The functional two-process-split realization + the **[V6]** N-1 old-broker × new-brain CI compat test are carried as `RESTORATION-PLAN.md` tasks D1–D7 (the compat test lands as a D7 gate).\n## CONTEXT-MEMORY.md\n 40│\n*41│The live/project authoring directives are **core-owned** — fed to every Psyche by spt-core (part of the composed psyche-download the daemon writes to the turn's `{psyche_context_file}`), so the split is consistent across all adapters (inconsistent rules → sync chaos). Adapters add only harness mechanics (how to write the drop file).\n 42│\n 43│## memformat (deeply integrated, self-evolving, the split scaffold)\n 44│\n   │...\n 69│\n*70│`Concepts`/`Aliases` make context **retrievable**; `Applies to` enables fine-grained scoping + query-routing; `Source` carries provenance; `Pinned` drives start-injection density (below).\n 71│\n 72│## Retrieval Map (generated INDEX)\n 73│\n*74│A generated, lightweight **INDEX** over topics — summaries + concepts + aliases + paths — **not** a vector DB, **not** a full mirror (adapted from pi-agent-memory's Retrieval Map). One INDEX per **project** (aggregating all agents' project context → realizes cross-agent synthesis + query-routing: \"which endpoint owns this task\") and per **agent**. Regenerated on context change; the merge driver + INDEX regeneration keep it current.\n 75│\n 76│## Start-injection: dense working set, sparse tail\n 77│\n## DEBUG-CONVERGE-PLAN.md\n 32│  convergence table, exit codes, loopback + two-host tests.\n*33│- **Out (still deferred):** remote build orchestration, dashboards/TUI, adapter\n 34│  inclusion, stable-release convergence. Keep the first watcher narrow.\n 35│\n 36│## CLI\n   │...\n 53│trust-derived subnet membership introduced in `b9326f4` (self + pinned peers,\n*54│registry rows as enrichment) so a freshly-joined or eviction-decayed node is\n 55│still *expected* and shows up as a row, not a silent omission. `--nodes` gives\n 56│an explicit set for targeted tests.\n 57│\n   │...\n 87│   the responder's identity for free.\n*88│2. **Registry/presence enrichment.** If a node already publishes its applied\n*89│   version + provenance into a registry row (the rollout metadata carries\n 90│   `git_commit`/version provenance), the coordinator can read convergence from\n 91│   rows it already syncs — no new round-trip. Verify whether\n*92│   `relcache`/registry persists \"applied version\" anywhere queryable; if not,\n 93│   prefer option 1.\n 94│\n 95│Pick option 1 as the baseline (self-contained, testable on loopback); treat\n## DEFERRED.md\n  7│| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any \"scroll back further than the buffer\" user need |\n *8│| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |\n  9│| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |\n 10│| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |\n*11│| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one...\n 12│| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |\n 13│| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant s...\n 14│| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |\n*15│| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |\n*16│| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |\n 17│| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |\n*18│| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |\n 19│| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the ...\n 20│| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concr...\n*21│| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect,...\n 22│| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-u...\n 23│| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the \"no connection\" state in `subnet status` + the coming-online banner...\n 24│| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8...\n*25│| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruni...\n*26│| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n*27│| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite ho...\n 28│| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\n 29│| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache...\n 30│| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n*31│| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`w...\n*32│| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version ...\n 33│| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n 34│| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer \"serving subnet X?\", a...\n 35│| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures...\n   │...\n 40│| **On-disk `current_exe` reconcile after auto-rollback (the D6-2 reboot residual)**: readiness-gated auto-rollback (ADR-0018 Q7, restoration D6-2) is **record-driven selection** — after a rollback the broker's supervisor keeps spawning the last-known-good `.old-N` binary, chosen from the durable `RolledBack` record, so the rescue survives a reboot for free **with no file rename at the failure instant** (the brittle path the design rejected). But the bytes on disk at `current_exe` are STILL the quarant...\n*41│| **Quarantine operator override (`spt update apply --force` — the note-i escape hatch)**: D6-2's quarantine guard refuses re-applying a version that auto-rolled-back on this node (the `RolledBack` record is the marker; clears naturally when N+1 stages). But a version whose failure was **environmental** (a transient boot condition, not bad bytes) leaves the node **permanently allergic** to a genuinely-good version with no escape but staging a newer one. FIX = an explicit operator override (`spt update ...\n 42│| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers m...\n*43│| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's \"remind me at 3pm\" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today...\n 44│| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **aut...\n*45│| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n 46│| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47│| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n*48│| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n*49│| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n*50│| **Always-on endpoint kind + `#`-sigil addressing (ADR-0023; REQ-EP-8 / REQ-EP-9) — the core seam for adapter-bridged group/forum messaging**: a `[always-on]` manifest section declaring a resident, owner-less, addressable endpoint binary the daemon boot-launches + supervises **continuously** (independent of agent liveness, register-triggered, one per `<adapter>[:profile]`), which self-manages its `#`-addressed channel endpoints via the existing `api bind` and may call the already-ungated `endpoint wak...\n## DOCS-STRATEGY.md\n  30│- **Human dev:** two agents exchange a message end-to-end in **one command + minimal config**, < 10 min — the core primitive, whole-thing-first, then decomposed.\n *31│- **Dev-agent integrator:** a minimal **adapter** that satisfies the manifest + `api` contract and drives one lifecycle operation — the \"build a harness for spt-core\" hello-world.\n  32│\n  33│## Agent-consumable docs (spt-core specifics)\n  34│\n    │...\n  38│- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry the canonical path and drop human-only narrative.\n *39│- **Machine-readable manifest/contract schema** (JSON Schema) at a stable, discoverable path — agents validate their adapter manifests against it; the schema *is* documentation.\n  40│- **MCP doc/resource server** — the standout fit, given the harness contract is the core integration surface. Expose the contract + reference as **deterministic, side-effect-free MCP resources** (same request → same result), giving integrating agents structured knowledge without RAG fuzziness. Highest-leverage agent-docs move for spt-core.\n  41│- **CLI help as first-class agent docs** — agents primarily shell `spt api …`, so `spt <cmd> --help` must be complete, exampled, and machine-readable (a `--json`/structured help mode). The CLI surface is a documentation surface.\n  42│\n    │...\n  44│\n *45│Deployed to GitHub Pages on `spt-releases` (ADR-0014; truth + CI generation stay in the source repo). **Tier 1 (ships with v0.1):** mental model/overview · human quickstart (install → two agents message) · adapter quickstart (via `mock-adapter`) · **harness contract vertical complete** (public manifest reference + full `spt api` reference + install-on-demand bootstrap pattern) · shell adapter getting-started (`spt-shell-notify` as the live example) · CLI reference generated from clap, CI-gated · ...\n  46│\n  47│**Site generator: mdBook + custom theme CSS** (Rust-toolchain-coherent — one `cargo install mdbook` on the existing runners; the Rust Book IA this strategy copies *is* mdBook). Styling north star: **Astro Starlight** — copy its look/feel in the theme CSS, not its toolchain. Raw `.md` published alongside rendered pages (`/x.html` ↔ `/x.md`) for the agent-export convention; `llms.txt` / `llms-full.txt` / `manifest.schema.json` as static assets at site root.\n  48│\n    │...\n  82│- [Quickstart: two agents exchange a message](…/quickstart/messaging.md): one command, <10 min.\n *83│- [Quickstart: build an adapter](…/quickstart/adapter.md): minimal manifest + `api` contract.\n  84│- [Mental model](…/concepts/overview.md): daemon, endpoints, instances, subnet.\n  85│\n  86│## Capabilities      (one bullet per vertical, each → its own llms.txt)\n    │...\n 101│# Quickstart — two agents exchange a message, end to end, <10 min.\n*102│> Pick your path: **developer** (here) · **building an adapter / dev-agent** → /quickstart/adapter.md\n 103│\n 104│## 1. Install (one line)        curl -fsSL https://spt-core.dev/install.sh | sh\n 105│   Verify:                      spt --version      # spt 0.1.0 (proto v1)\n    │...\n 119│0 Getting started → 1 Quickstart (the early runnable project) → 2 Mental model\n*120│→ 3 Capability verticals (build-order) → 4 Capstone: build a non-CC adapter end-to-end\n 121│```\n 122│**Vertical ordering (dependency-sequenced):**\n 123│`Messaging → Lifecycle → Terminal → Networking → Harness contract → Instances → Shells → Self-update`\n    │...\n 140│/// The manifest's `min_spt_core_version` exceeds this binary.\n*141│/// **Fix:** update spt-core, or pin the adapter to a compatible release.\n 142│VersionTooOld { required: Version, found: Version },\n 143│```\n 144│\n## DORMANCY-BUDGET.md\n  5│**What this answers.** ADR-0003's Stage-A red-team #9: *\"dormant-warm default\n *6│has real resource/security cost — measure N dormant sessions on real adapters;\n *7│define an adapter-specific warm/cold policy + resource budget before locking\n  8│warm as the default.\"* This document records the measurement and locks the\n  9│policy with numbers instead of a guess.\n 10│\n   │...\n 36│Knobs: `SPT_BUDGET_N` (seats), `SPT_BUDGET_PROGRAM`/`SPT_BUDGET_ARGS` (the\n*37│seat command — point at the real adapter's harness), `SPT_BUDGET_SETTLE_MS`,\n 38│`SPT_BUDGET_IDLE_MS`. Rig runs ride the CI `[budget]` commit-message gate\n 39│(both hosts measure in one tagged push) or a direct local\n 40│`SPT_BUDGET=1 cargo test -p spt-daemon --test budget --release -- --nocapture`.\n   │...\n 53│\n*54│3 × `claude` (real adapter seat — the heavy case):\n 55│\n 56│| metric | per-seat |\n 57│|---|---|\n   │...\n 69│5 × `sh` (shell seat — no LLM harness installed on this host, so the shell\n*70│floor is its measurement; the adapter-class number generalizes from\n 71│HFENDULEAM's, the harness binary being the same node runtime there):\n 72│\n 73│| metric | per-seat |\n   │...\n 84│1. **Warm (dormant) stays the default.** An idle warm seat burns **zero CPU**\n*85│   on both adapter classes; the cost is purely resident memory. A shell-class\n 86│   seat is ~8 MiB — effectively free at any realistic seat count. A heavy\n 87│   LLM-harness seat is ~300 MiB — a workstation-class node holds ten warm\n 88│   seats in ~3 GB, acceptable for the instant-reactivation win\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n  10│seam) are spt-core changes** (todlando's wave, sequenced after F-029). **The\n *11│trust-seeder binary itself is adapter-side** (perri's wave — spt-core stays\n *12│harness-agnostic per the glue-model boundary: adapters = manifest + binary only;\n  13│no harness-aware source in spt-core).\n  14│\n  15│---\n    │...\n  43│| 2 | ACCOUNT-ROOT-MISMATCH — trust seeded in a DIFFERENT config root than the spawn resolves; wedges despite a valid-looking seed | Reavo/Brandon dual-root box (si1 vs b6b probes) | SILENT |\n *44│| 3 | PSYCHE-CRASHLOOP — nested psyche CC dies at trust dialog; wrapper respawned ~3/s, ordinal 5358 in 30 min | F-h; now guarded by adapter v0.13.0 give-up + core C3(b) | SILENT (guarded) |\n *45│| 4 | ADAPTER-UNRESOLVED — no registered adapter hosts the parent binary | seed→listen path, bash parent | LOUD pre-spawn (good shape) |\n  46│| 5 | STALE-CLI-CONTRACT — bringup recipe passes a removed flag; \"unexpected argument\"; spawn never happens | old recipe on 0.22.0+ | LOUD pre-spawn (good shape) |\n  47│| 6 | RELAUNCH-NEVER-BINDS — 2nd `endpoint run` same id after tree-kill; prints but never binds | **0.22.0 only — FIXED on 0.23.0 by F-028 B1/B4** (perri definitive repro 2026-07-03: tree-kill → same-id re-run re-binds fresh session, live PTY, honest info.json; first check was rig-void, --subnet omitted, caught before reporting) | FIXED-NOTE (not in F-027 scope) |\n  48│| — | UAC-740 elevation spawn-fail | NOT observed (subnet-op elevation only) | out of scope |\n    │...\n  93│  deadline separates cleanly at ≥30 s; **never configure below 30 s**),\n *94│  overridable per-adapter via a manifest key (the bringup-time contract\n *95│  belongs to the adapter) and per-invocation via `--bind-deadline <secs>`\n  96│  (the per-node escape for cold-broker first-spawn / loaded boxes). Escape\n  97│  hatch `--no-wait` restores fire-and-forget and prints\n  98│  `ENDPOINT_RUN_DETACHED:{id}` (explicitly unverified — never `_STARTED`).\n    │...\n 105│\n*106│### Half B — pre-spawn manifest seam (spt-core executes; adapter owns the binary)\n 107│\n 108│New manifest key **`pre_spawn`** inside `[session.<role>]` tables (applies to\n 109│`self` AND `resume` — field-confirmed necessary on resume: a resume is a\n    │...\n 115│1. **Env parity via capture-at-creation (BINDING; sharpened by perri review):**\n*116│   env vars the adapter declares `[env.<NAME>] direction=\"read\"` are **captured\n 117│   at endpoint creation and stamped into the perch record**; the broker threads\n 118│   the IDENTICAL stamped values to BOTH the pre-spawn command AND the session\n 119│   spawn. Neither consumer reads its own process env for a stamped var — if the\n    │...\n 126│   `[env]` declares, knowing nothing of `CLAUDE_CONFIG_DIR` specifically.\n*127│   (Field-confirmed adapter shape: claude-spt declares\n 128│   `[env.CLAUDE_CONFIG_DIR] direction=\"read\"` with `~/.claude` fallback —\n*129│   adapter reads, never sets.)\n 130│2. **Install-dir-first resolution (REQ-INSTALL-11):** the program path resolves\n*131│   via `resolve_program_in_dir` like every other adapter-manifest program.\n 132│3. **Bounded + loud:** runtime capped (30 s); non-zero exit or timeout ABORTS\n 133│   the session spawn with `ENDPOINT_SPAWN_FAIL:{id}: reason=PRE_SPAWN_FAIL:\n 134│   <exit/detail>` on CLI stderr (relayed through the spawn RPC error — this\n    │...\n 163│  owlery cwd is trust-seeded by the same pre-spawn seam, and the crashloop\n*164│  backstop (C3(b) + adapter give-up) already guards the symptom.\n 165│- No mode-6 work: FIXED on 0.23.0 by F-028 B1/B4 (perri definitive repro).\n 166│  The Half-A deadline token still catches its shape if it ever regresses.\n 167│\n    │...\n 174│| 3 PSYCHE-CRASHLOOP | Half B (owlery cwd seed) | existing C3(b)/v0.13.0 guard |\n*175│| 4 ADAPTER-UNRESOLVED | — (already good) | existing pre-spawn error |\n 176│| 5 STALE-CLI-CONTRACT | — (already good) | existing / Half A `EXITED_PRE_BIND` |\n 177│| 6 RELAUNCH-NEVER-BINDS | FIXED (F-028 B1/B4, 0.23.0-verified) | Half A `NO_SESSION`/`WEDGED_PRE_BIND` (regression net only) |\n 178│\n    │...\n 184│  (never binds) → assert today's silent `ENDPOINT_RUN_STARTED` becomes the\n*185│  token. Real-daemon int candidate (mock-adapter seam exists — F-028 pattern).\n 186│- `REQ-PRESPAWN-COMMAND-SEAM` — manifest `pre_spawn` executes with spawn-env\n 187│  parity + install-dir-first resolution + bounded runtime; failure aborts\n 188│  loud. Red-first unit on env parity (seeded sentinel var visible to pre-spawn\n    │...\n 191│  bug, perri 2026-07-03): the conformance test IS the Half-A red-first int.\n*192│- Adapter-side (perri's repo, NOT a core REQ): trust-seeder contract —\n 193│  forward-slash exact key, root-as-spawn-resolves, idempotent.\n 194│\n 195│## 6. Docs surfaces (docs-drift gate applies)\n    │...\n 218│   the session spawn consume the stamp — neither reads its own process env.\n*219│   Adapter shape confirmed: `[env.CLAUDE_CONFIG_DIR] direction=\"read\"`,\n*220│   fallback `~/.claude`, adapter reads and never sets.\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n   3│Status: **READY FOR BUILD 2026-07-04 — wave called by operator.** Author: doyle.\n  *4│Inputs: perri `PSYCHE-EPHEMERAL-PLAN.md` (adapter half, doyle-gated PASS; built on\n   5│parked branch `psyche-ephemeral-parked`, publish held for this core wave), the\n   6│2026-07-04 field case (both directions — v0.13.2 bad-ship + hall-bf multi-subnet\n   7│poll refusal, evidence table §2), doyle code recon (anchors verified @f8428a8).\n    │...\n  10│spt-core** (todlando's wave). **The psyche role binaries/templates are\n *11│adapter-side** (perri's parked branch — publishes ONLY after the core floor\n  12│releases; see §7 sequencing, a BINDING lesson from the v0.13.2 bad ship).\n  13│\n  14│---\n    │...\n  35│\n *36│**(a) The adapter cannot HOLD residency.** The resident runner's per-pulse\n  37│liveness poll (`spt ready <id> --once`) hard-refuses on a multi-subnet home\n  38│(`READY_FAIL: … pass --subnet`). The nested psyche process **cannot know its\n  39│subnet/home**: core fills only `base_keys` = `{id}`, `{session_id}`, `{node}`\n    │...\n  42│the daemon re-hosted, ~5 s churn forever: hall-bf-psyche ordinal 6491+, at\n *43│one point 54 stamped host attempts. Adapter v0.13.4 made the death LOUD\n  44│(`PSYCHE_POLL_FAIL`), but residency remains structurally impossible there.\n  45│\n  46│**(b) The daemon EXPECTS residency, and that expectation is destructive.**\n *47│Adapter v0.13.2 accidentally shipped the parked ephemeral shim: its by-design\n  48│exit after the seed turn tripped the residency machinery, which tore down the\n  49│endpoint's hosted state — ready marker removed, never re-stamped, every\n  50│force-native gated `leg=cli-gate-not-hosted` **permanently** (field brick,\n    │...\n  58│- **`.live-bin` own-copy:** exists solely so the long-lived resident's image\n *59│  doesn't Windows-image-lock the adapter registry dir during `spt adapter\n  60│  update` (the crc-swap `.old`-litter hazard class). Ephemeral turns hold the\n  61│  image only for a bounded turn — the own-copy machinery loses its reason.\n  62│- Psyche TURNS already work daemon-driven today (`psyche_download`, revival\n    │...\n  68│|---|----------|-------|-----------|\n *69│| 1 | `LIVEHOST_PSYCHE_NONRESIDENT` xN in EVERY iso-home run, both adapter versions | perri RC + wakefin rigs | daemon expects residency |\n *70│| 2 | hall-bf-psyche boot-churn ~5 s cadence, ordinal 6491+, synthetic `sess-<pid>-<ts>` ids, no real session ever binds; starts exactly at BIGNET join (7/3) | operator box, real home | adapter can't hold residency |\n *71│| 3 | Poll-verb root repro: `spt ready <id> --once` on 2-subnet home → exit 1, `READY_FAIL … pass --subnet`, empty stdout | perri, clean repro | adapter can't hold residency |\n  72│| 4 | v0.13.2 shim-exit → residency teardown → ready removed → permanent `leg=cli-gate-not-hosted` | hall-bf + perri wake-confirm | residency expectation destructive |\n  73│| 5 | Psyche turns land fine via daemon-driven paths while the resident churns | hall-bf digest (`psyche_download` at every boundary) | per-event shape already proven |\n  74│| 6 | ~12/min re-host cadence NEVER tripped the C3(b) thrash guard (boot records are apparently not ledger boundaries to the guard) | hall-bf | guard blindness (rides this wave) |\n    │...\n 160│\n*161│Remove the own-copy spawn path for psyche roles (spawn from the registry\n*162│install dir like every other bounded adapter invocation — image-lock exposure\n 163│is now turn-bounded, same class as `translate`/`digest` today). GC existing\n 164│`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is\n 165│image-locked at update time). Compat: a manifest that still names the\n    │...\n 185│\n*186│- No adapter-aware code in core: core wires WHEN turns run and WHAT keys they\n*187│  get; the adapter's parked branch owns what a psyche turn does (glue-model\n 188│  boundary holds).\n 189│- No relay-unregister fix (the post-clear `owl send → NO_PERCH` casualty) —\n 190│  separate seed, C-2 follow-up (a), field-repro'd 2026-07-04, sequenced with\n    │...\n 205│| Thrash-guard blindness (12/min invisible) | W3 ledger-counted budget | red-first synthetic loop trips it |\n*206│| `.live-bin` image-lock class | W5 (bounded turns from registry dir) | existing crc-swap displace+GC net |\n 207│| Reseed loop (custody lost repeatedly) | W2 reseed + W1 budget | `PSYCHE_RESEED` marker + budget stamp |\n 208│\n 209│## 5. Requirement seeds (register at wave start, `required_stages=[]` until the chunk activates)\n    │...\n 216│  survives the turn.\n*217│- `REQ-PSYCHE-SID-CUSTODY` — psyche templates receive the psyche's OWN sid;\n 218│  parent boundary does not rotate it; reseed is loud. Red-first: parent\n 219│  `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's\n 220│  — guard-revert reproduces).\n    │...\n 227│  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,\n*228│  failing psyche → parent stays deliverable, no rehost churn, error stamped.\n 229│- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the\n 230│  failure budget counts real attempts (ledger-derived); a 12/min failure loop\n 231│  trips it. Red-first synthetic loop.\n*232│- Adapter-side (perri's repo, NOT core REQs): parked-branch ephemeral roles;\n 233│  `min_spt_core` floor = the release that ships this wave; publish gated on\n 234│  that floor being LIVE (bad-ship lesson, binding).\n 235│\n    │...\n 244│  **single-node multi-subnet rig** (the hall-bf shape: 2 subnets, one home).\n*245│- **Adapter (perri):** `psyche-ephemeral-parked` branch rebased once core API\n 246│  shape freezes (she reviews W1/W2 key contract before her rebase — field\n 247│  review slot like F-027 §7); publish ONLY after core floor releases.\n 248│- **hall-bf interim:** stays turn-functional + visibly stamped (v0.13.4) until\n## F-035-CONFLICT.md\n  3│**Raised by:** todlando, mid-build, 2026-07-09. **Build HELD** (WIP uncommitted on\n *4│`f-035-window-drain-honor`). doyle's COLLAPSE ruling collides with a **shipped**\n  5│acceptance gate my RCA (docs/F-035-RCA.md) missed. Need an A/B/C ruling before I proceed.\n  6│\n  7│---\n   │...\n 13│\n*14│**`crates/spt/tests/idle_edge_drain_e2e.rs:89`** — `fn spool_while_active_then_idle_fires_injection`\n*15│`// [int->REQ-MSG-IDLE-EDGE-DRAIN] [int->REQ-HAZARD-DELIVERY-STARVATION]`\n 16│\n*17│- **Setup:** spools `spool_message_deferred_at(perch, \"peer\", …)` — which is\n*18│  `insert_message(…, WINDOW_ACTIVE_ONLY, …)` (spool.rs:363). So the row is\n 19│  **`active_only`**.\n 20│- **Trigger:** `spt api state idle <id>` — the real ACTIVE→IDLE edge.\n 21│- **Assertion (the keystone):** `assert_eq!(pending_after, 0, …)` — the `active_only`\n*22│  row **MUST be delivered** on the idle edge (spool cleared).\n 23│\n*24│My fix → the `active_only` row is excluded from the idle claim → stays spooled →\n 25│`pending_after == 1` → **this shipped gate goes RED.**\n 26│\n*27│`REQ-MSG-IDLE-EDGE-DRAIN` (F-023 leg 2) and `REQ-SEND-WINDOW-DRAIN-HONOR` (F-035) are\n 28│directly incompatible for the relay-less spt-hosted class. The active/idle asymmetry I\n 29│called a bug in the RCA is **intentional** for that class.\n 30│\n   │...\n 35│For an **spt-hosted** endpoint (broker owns the PTY, `controllable`, relay-less), the\n*36│idle-edge/parked drain is the **ONLY** carrier for an `active_only` row. Traced every path:\n 37│\n 38│| carrier | `active_only` to spt-hosted? |\n 39│|---|---|\n*40│| Send-time inject (`cli.rs:5716` `try_inject = prefer_native \\|\\| !active_only`) | **NO** — cli.rs:5713 verbatim: *\"a non-native `--active-only` is the explicit hook channel: **spool-only, no inject attempt**.\"* |\n 41│| Send-time idle re-offer (`cli.rs:5734`) | **NO** — gated `&& !active_only`. |\n*42│| Hook poll `drain_active_window` (`api poll`) | **N/A** — that is the HARNESS-hosted hook channel; an spt-hosted endpoint has no `api poll` harness. |\n*43│| **Idle-edge / parked drain `drain_idle_spool`** | **YES** — the F-023 leg-2 behavior (`include_deferred = !resting`). **This is the only carrier.** |\n 44│\n 45│So: `active_only` is designed as *\"the active **hook** window\"* — meant for\n 46│**harness-hosted** endpoints that `api poll`. An **spt-hosted** endpoint has no hook, so\n   │...\n 59│  `active_only`+spt-hosted is knowingly revoked (that class starves, or gets an\n*60│  evaporate/refuse-at-send policy instead). Requires re-scoping `REQ-MSG-IDLE-EDGE-DRAIN`\n 61│  + rewriting its int.\n 62│- **(B) never-starve wins** — the idle edge *should* carry `active_only` to spt-hosted\n 63│  (F-023 correct). Then **F-035 is mis-scoped**: *\"never wakes\"* is the **harness** hook\n   │...\n 65│  never a contract for the hook-less spt-hosted class. **RETIRE or NARROW\n*66│  REQ-SEND-WINDOW-DRAIN-HONOR; no code change.**\n*67│- **(C) class split** — spt-hosted delivers `active_only` at idle **without waking**\n 68│  (non-interrupting background inject) vs harness-hosted holds-for-poll. New behavior,\n*69│  larger; needs a \"deliver-but-don't-wake\" inject mode the translation binary honors.\n 70│\n 71│**todlando lean:** the carrier table points at **(B)** — the field-doc *\"never wakes an\n 72│idle target\"* (cli.rs:85) describes the harness hook model, and F-023 already reasoned\n   │...\n 74│`--deferred` background row reaching an idle spt-hosted agent) may be **working as\n*75│designed** — it is the only way that class can ever be delivered. If the true concern is\n*76│*\"it interrupts,\"* that is **(C)**, not the drain-exclusion of **(A)**. But this is your call.\n 77│\n*78│My WIP (spool.rs window-axis collapse + inject.rs + 2 units) is staged **uncommitted** — I\n*79│touched neither delivery.rs nor worker.rs. Standing by for A / B / C.\n## F-035-RCA.md\n *1│<!-- [doc->REQ-SEND-WINDOW-DRAIN-HONOR] F-035 RCA — report-before-fix. -->\n *2│# F-035 RCA — idle-edge parked-drain ignores the delivery-window tag\n  3│\n  4│**Status:** RCA-first, **report before fixing** (doyle-dispatched 2026-07-09).\n *5│**REQ:** `REQ-SEND-WINDOW-DRAIN-HONOR` (registered, `required_stages = []` — activate on doyle's fix ruling).\n  6│**Investigator:** todlando. **Off:** `origin/main@2c05dc9` (v0.30.2).\n  7│\n  8│---\n   │...\n 20│\n*21│For an **spt-hosted, relay-less** endpoint (the class the idle-edge drain targets),\n*22│such a row is delivered through the translation binary at the idle window — the\n*23│exact delivery the tag forbids.\n 24│\n 25│---\n 26│\n   │...\n 28│\n*29│The two delivery-window surfaces are **asymmetric**. One honors the `window` axis;\n 30│the other keys on the legacy `deferred` mirror and **never consults `window`**.\n 31│\n*32│### The delivery-window model (ADR-0028 W2, `spt-store/src/spool.rs`)\n 33│\n 34│| window        | active hook poll | idle / wake |\n 35│|---------------|:----------------:|:-----------:|\n   │...\n 40│`deferred` is a **retained back-compat mirror**: `deferred = 1` **IFF**\n*41│`window = 'active_only'` (`deferred_for`, spool.rs:46).\n 42│\n*43│### ACTIVE drain — honors the axis (correct)\n 44│\n*45│`spool::drain_active_window_inner`:\n 46│\n 47│```\n*48│include_deferred ? \"WHERE delivered=0 AND \\\"window\\\" != 'idle_only'\"\n*49│                 : \"WHERE delivered=0 AND \\\"window\\\" = 'default'\"\n 50│```\n 51│\n 52│Filters on **`window`** — `idle_only` is excluded from the hook poll. Correct.\n 53│\n*54│### IDLE-edge drain — ignores the axis (the bug)\n 55│\n*56│`spool::claim_idle_edge_inner` (reached via `spt_daemon::inject::drain_idle_spool`\n*57│— the **one shared leg** the active→idle edge drain `delivery.rs:drain_idle_window`,\n 58│the send-time already-idle path, and the pulse re-offer belt all call):\n 59│\n 60│```\n*61│include_deferred ? \"WHERE delivered=0\"\n*62│                 : \"WHERE delivered=0 AND deferred=0\"\n 63│```\n 64│\n 65│Filters on the **`deferred` flag only** — **never on `window`**.\n 66│\n*67│`drain_idle_spool` passes:\n 68│\n 69│```\n 70│let include_deferred = !crate::resting::deferred_held(&perch_path);\n   │...\n 73│So whenever the endpoint is **not resting**, `include_deferred = true` → the idle\n*74│claim is `WHERE delivered=0` → it takes **`active_only` (deferred=1)** rows and\n 75│injects them at the idle window.\n 76│\n 77│### Why it slipped through\n## F-035-RELAY-RCA.md\n  5│live agent WITH a relay (lia: relay-for-idle, poll-for-busy). Corrected contract:\n *6│**`active_only` = poll-only for a relay-bearing live agent — NEVER relay-delivered.**\n  7│\n  8│---\n  9│\n   │...\n 11│\n*12│I traced every way an `active_only` row could reach lia's relay (its live PollListener\n*13│via `deliver_tcp`, OR its backlog via `drain_backlog`). **All are closed on current main:**\n 14│\n 15│| path | carries active_only to the relay? | why not |\n 16│|---|---|---|\n*17│| Local `spt send` (`cmd_send`→`send_windowed`) | **NO** | `deliver.rs:217` — `if window != WINDOW_ACTIVE_ONLY && deliver_tcp(...)`. active_only skips the live TCP, spools. |\n*18│| Cross-node `spt send` (WAN) | **NO** | `cli.rs:5762` — a remote active_only that misses locally returns *\"active-only stays local-only\"* and does NOT `wan_send`. The `WanMessage` wire record has **no window field** (wanmsg.rs:38), and the sender deliberately keeps active_only local-only rather than drop the class over the wire. So active_only never reaches `receive_wan`/`deliver_tcp` (wan.rs:300). |\n*19│| Relay backlog (`relay.rs:63 drain_backlog`) | **NO** | `drain_non_deferred` = `WHERE delivered=0 AND deferred=0`; `deferred=1 IFF active_only` (spool.rs:46). Excluded. (doyle already confirmed.) |\n*20│| Relay live-poll-forward (`relay.rs:77 pump_once`) | **NO** | Its only feed is `deliver_tcp`→the PollListener; no unguarded `deliver_tcp` of an active_only user msg exists (`deliver::deliver` callers = ring/ready ping-pong + tests; `reporting.rs:456` is a notif frame; `reporting.rs:161` shell-context uses `send_deferred`→**spool**, not tcp). |\n*21│| Parked-idle re-offer belt (`livehost.rs:265 drain_idle_spool`) | **NO** | BELT-1 `relay_present → Skip` (livehost.rs:495); and `claim_idle_edge` (post-nothing) is guarded `is_spt_hosted_no_relay`. lia has a relay → never runs. |\n*22│| busy→idle relay start (`relay.rs:90 run`→`drain_backlog`) | **NO** | same `drain_non_deferred` exclusion — on going idle, only `default`+`idle_only` spooled-while-busy rows drain to the relay; active_only stays spooled for the next busy poll. |\n 23│\n*24│The **only** carrier for a spooled active_only row is lia's hook **poll**\n*25│(`api poll`→`poll_drain`→`drain_active_window`, `WHERE \"window\" != 'idle_only'`, which\n*26│includes active_only) → delivered to the **polling session's stdout**, never the relay.\n 27│\n*28│**Conclusion:** on current main, spt-core's send/deliver/relay/spool/poll legs do **not**\n*29│leak active_only to a relay. Corrected-scope `REQ-SEND-WINDOW-DRAIN-HONOR` (poll-only for\n 30│a relay-bearing live agent) appears **already satisfied** by these legs.\n 31│\n 32│---\n   │...\n 41│   version-lag, not a current-main bug (verify-on-upgrade, no code change).\n*42│2. **Adapter-side handoff.** The busy↔idle **poll→relay handoff** is the adapter's\n*43│   (spt-claude-code) job, not spt-core's `api state idle`. If the adapter does a final\n*44│   `api poll` on going idle (draining active_only) and injects the result into the\n*45│   now-idle session, that's an adapter route — outside these legs. → perri's lane.\n 46│3. **A repro detail I'm missing.** → Need the exact send form (`spt send --active-only`?\n 47│   a shell-context/`send_deferred`? a notify?), the sender's node vs lia's node\n 48│   (same-node/cross-node), and lia's controllable/relay state at send time.\n## FAULT-MATRIX.md\n 20│| 1 | **Brain killed mid-PTY-stream** (logic crash / routine self-update) | Logic halts until restart | Broker, PTY child (pid stable), output log | New brain re-subscribes from cursor; gapless + exactly-once | `spt-daemon/tests/` B2/B9 handoff suite; `tests/idempotent.rs` (crash before-intent / before-effect / after-effect) |\n*21│| 2 | **Brain killed mid-QUIC-stream, receive side** | Logic halts | Broker-owned endpoint, conn, stream, read ring | Resubscribe from durable cursor; ring replays the dead window | `tests/netstream.rs::receiver_brain_restart_is_gapless_and_exactly_once` |\n 22│| 3 | **Brain killed mid-QUIC-stream, send side** | Logic halts | Broker-owned conn/stream + effect journal | Re-drive whole durable op sequence; journal dedups replays (no dup on the wire) | `tests/netstream.rs::sender_brain_restart_redrive_is_exactly_once` |\n 23│| 4 | **Brain dead across peer connect/disconnect** | Presence consumption pauses | PresenceLog ring buffers liveness transitions | Resubscribe from presence cursor; missed transitions replay | `tests/presence.rs` (both tests) |\n 24│| 5 | **Peer node vanishes (graceful close)** | That conn's streams end | Local broker, other conns, brain | Closed-watcher removes conn row + emits `disconnected`; dial fresh when needed | `tests/presence.rs`; `nethost.rs` closed-watcher |\n*25│| 6 | **Registry feed: stale/replayed update over the wire** | None — update dropped | Stored registry state (newer epoch holds) | None needed (epoch lease absorbs it) | `tests/replicate.rs::registries_converge_over_the_wire_and_the_lease_holds`; `replicate.rs` unit tests |\n*26│| 7 | **Registry feed: corrupt record in the stream** | That record only — skipped | The rest of the feed (decoder does not wedge) | Lease makes loss safe; next update supersedes | `replicate.rs::decoder_survives_chunk_splits_and_corrupt_lines` |\n 27│| 8 | **Net disabled / endpoint bind fails at boot** | No WAN; net frames answer `enabled:false` / typed error | PTY hosting (daemon degrades to net-less broker) | Fix config, restart daemon | `broker.rs::dispatch_net_status` (None arm); `Daemon::run` degrade path |\n 28│| 9 | **Broker process restart (journal survives, conns don't)** | Live conns/streams lost | Effect journal (durable) | Deduped op whose resource is gone → typed \"retry with a fresh op_id\" error; brain re-dials; an `spt rc` operator viewport RECONNECTS on its own — centered banner, full establish re-drive inside a bounded window, then a plain-language give-up (REQ-RC-RECONNECT) <!-- [doc->REQ-RC-RECONNECT] --> | `broker.rs::dispatch_net_dial` / `dispatch_net_stream_open` restart arms; `rc.rs` reconne...\n*29│| 10 | **WAN msg feed replayed** (sender redrive, or receiver brain restart resubscribing from a stale cursor) | None — replayed records dedup | Spool state (each op exactly one row); already-delivered ops | Durable `wan_seen` op-id claims absorb the replay; spool path claims atomically with the row | `tests/wanmsg.rs::receiver_restart_replays_feed_without_double_delivery`; `spool.rs` wan unit tests |\n 30│| 11 | **WAN msg record forges its origin** (payload `origin_node`/`node` field) | None — forgery inert (unknown field) | Access-gate subject (handshake-proven `remote_id_hex`) | None needed (records carry no origin field by design — KH 7.5) | `tests/wanmsg.rs::wan_message_lands_exactly_once_under_transport_origin`; `wanmsg.rs::forged_origin_field_is_inert` |\n 31│| 12 | **Target brain killed mid-remote-attach** (operator typing into the dead window) | Pumping pauses until handoff | Broker-held QUIC stream, session + child, output log, effect journal; the operator's viewport | Successor re-serves the same stream from seq 0 (worst case): replayed input dedups at the PTY-write journal; re-transmitted output dedups at the operator's render cursor — every byte exactly once | `tests/attach.rs::attach_survives_target_brain_restart_exactly_once` |\n*32│| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\n 33│| 14 | **Stream lost mid-context-bundle-sync** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's context store (an incomplete bundle is never fetched — length-gated, scratch file deleted); the responder's store untouched | No resume protocol by design: re-pull fresh — the apply is ancestry-idempotent (an already-joined tip short-circuits; fetched tips quarantine under `refs/spt-sync/` so a partial fetch never clobbers `refs/heads/`) | `tests/sync.rs...\n 34│| 15 | **Partition: concurrent context writes on two nodes** | The file's auto-propagation pauses (conflict surfaced, not propagated) | BOTH versions — local file untouched on each node + the other's version as a durable tracked artifact (hazard 6.6); every other file keeps syncing | Elected reconciler (Active instance's node; lowest-id fallback) runs one bounded Psyche turn → merged write `join(vA,vB)+bump` dominates both parents → propagates as a plain accept, clears artifacts subnet-wide | `test...\n 35│| 16 | **Reconcile turn fails** (harness absent / timeout / garbage or empty output / 6.5-suppressed write) | That file stays in conflict-holding state | Both versions (artifacts + local file) — exactly where they were; the store, the sync loop | Retry at the next sync/activation; a fresher direct write re-merges against the new local version | `spt-live reconcile.rs::failed_turn_preserves_everything` / `suppressed_write_preserves_artifacts`; `turn.rs::empty_stdout_is_an_error` |\n*36│| 17 | **Registry ambiguity during reconciler election** (partition: two nodes both see Active / no Active anywhere) | Two textually-different merges of the same pair may both mint | Neither version is ever lost — merged writes carry joined vectors, so the two merges classify **concurrent** and re-surface as a new conflict pair (detected, not silently last-wins) | Deterministic tiebreak bounds the storm (lowest node id among Actives / non-Offline holders); the re-surfaced pair reconciles on the next pa...\n 37│| 18 | **Compromised relay node serves a tampered update artifact** (bytes corrupted at rest after the relay's own verification) | That one pull ends `Rejected(ArtifactMismatch)` at the puller | The puller's staged release + running binary (nothing staged, nothing applied — no code path from wire bytes to the cache except through `plan_verified`); the rest of the subnet pulls from honest peers | Pull from another peer; the per-node gate (REQ-UPD-2) runs at **every** hop, so one poisoned node never re-p...\n 38│| 19 | **Non-conforming peer offers a rollback/expired/off-channel release** | None — rejected pre-fetch, zero artifact bytes move | The puller's version (monotonic floor holds); the wire (no transfer ever starts) | None needed — the offer-then-fetch shape gates metadata before bytes (REQ-HAZARD-UPDATE-ROLLBACK) | `tests/propagate.rs::rollback_offer_is_rejected_before_any_fetch` |\n 39│| 20 | **Untrusted node queries for the staged update** | None — refused by not offering (fail-closed, the up-to-date shape: learns nothing, not even whether a release exists) | The staged release; the serve loop (one refusal, no state) | None needed; pair the node to entitle it | `tests/propagate.rs::untrusted_origin_gets_no_offer` |\n   │...\n 41│\n*42│| 22 | **Psyche emits spoofed routing** (`<EVENT type=\"reply\" from=\"evil\" to=\"victim\">` on its stdout) | None — the spoof is structurally inert: the intent parser carries body only (attrs unrepresentable), the daemon re-stamps `from=<psyche_id>` and routes to the inbound message's structural sender (its `from`) alone | The spoof target (receives nothing); the wire (only daemon-authored envelopes); every other agent's identity | None needed (anti-spoof is structural + re-stamp, ADR-0012 / KH 7.3) | `psy...\n*43│| 23 | **Psyche replies with no inbound sender to answer** (alarm-fired turn emits a reply) | That one intent drops with a typed outcome (fail-closed — never broadcast, never guessed) | Every perch (zero deliveries); the notify leg (independent) | The Psyche's next answered turn replies normally | `psyrelay.rs::reply_without_target_is_dropped` |\n*44│| 24 | **Live-Psyche driver loses stdout** (the 7.3 null-stdout shape: detached/`Stdio::null()` regression or a turn that produced nothing) | That turn fails loudly (`TurnError::EmptyOutput`); nothing relays | The relay boundary (no silent success — indistinguishable-from-bug shapes are errors); the spool | Fix the driver / retry the turn | `psyrelay.rs::null_stdout_driver_fails_the_guard`; `spt-live turn.rs::empty_stdout_is_an_error` |\n*45│| 25 | **Runaway Psyche dumps an oversize body** (> 64 KiB in one intent) | That intent drops with a typed outcome | Delivery substrate (never asked to carry it); other intents in the same turn relay normally | None needed (legitimate long bodies chunk at delivery — T3 `EVENT-PART`) | `psyrelay.rs::empty_and_oversize_bodies_drop_typed` |\n*46│| 26 | **Notif feed replayed whole** (re-presented rows after loss/restart — the push-feed's normal recovery) | None — the semilattice join no-ops (`Unchanged` per row, nothing written) | The spool (no duplicates, no state regression) | None needed: replay IS the recovery protocol (full-row re-presentation, no delta bookkeeping) | `tests/notifsync.rs::notif_spools_converge_over_the_wire_and_dismiss_replicates` (replay leg); `notifsync.rs::feed_converges_two_stores_and_dismiss_replicates` |\n 47│| 27 | **Concurrent dismiss + surface on two nodes** (the same notif dismissed on A while B marks it seen/surfaced) | None — the writes commute through the join (OR/union/max); both survive the bidirectional exchange | Both stores converge to identical rows; the dismiss latch (a stale undismissed copy can never un-dismiss) | None needed (no conflict exists to surface — every field is monotone) | `notifsync.rs::concurrent_dismiss_and_surface_commute_across_the_feed`; `spt-store notif.rs::merge_is_idem...\n*48│| 28 | **Untrusted origin injects notif records** (unpaired peer, or a `home`-trusted peer spoofing into `work`) | None — dropped fail-closed at the apply gate, zero rows written; the gate's subject is the handshake-proven stream-table origin, never payload bytes (KH 7.5) | The spool (nothing materialized); every subnet the origin isn't trusted in (trust is per-subnet) | None needed; pair the node to entitle it | `tests/notifsync.rs` (gate-negative leg); `notifsync.rs::untrusted_and_cross_subnet_origin...\n*49│| 29 | **Notif record for a non-member subnet** (a peer feeds rows for a subnet this node never joined) | None — dropped fail-closed; the subnet is never materialized locally (the REQ-INST-13 posture: unconfigured replicates nowhere) | The member subnets' spools; the feed (other records still apply) | None needed | `notifsync.rs::non_member_subnet_record_never_materializes` |\n 50│\n 51│## Planned rows (accepted debt — no injection evidence yet)\n 52│\n   │...\n 57│| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\n*58│| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\n*59│| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\n*60│| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\n 61│| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\n*62│| P8 | **Receiver crash between a live-TCP WAN delivery and its op-id claim** | That one in-flight op re-delivers on replay (at-least-once window — chosen over claim-first, whose crash gap silently *drops*; see `wan.rs` module docs) | Spool path (claim+row atomic); all other ops | live-listener durable receipt, if the window ever bites |\n 63│\n 64│**Maintenance rule:** when a planned row gains a real injection test, move it up with\n*65│its evidence path. When new subsystems land (D5 WAN delivery, D6 Psyche sync), add\n 66│their fault rows *in the same change* — a subsystem without a row here is a review\n 67│finding.\n## FLAKE-LEDGER.md\n 16│| 7 | `spt-daemon::attach attach_survives_target_brain_restart_exactly_once` | 1× kitsubito (2026-06-16 run 27595180782 attempt 1, M11-W3) | **TIMEOUT, not an assertion fail** — SLOW>60>120>180>TERMINATING, nextest TIMEOUT at the 240s cap (`attach.rs:600`). A heavy multi-process brain-restart-exactly-once test on the slow shared kitsubito box under concurrent load (n1-gate on the same runner + a parallel `ci.yml` docs-publish run firing simultaneously). W3-INDEPENDENT: Windows green end-to-end; ALL tu...\n*17│| 8 | CI build step `Build notify-shell` (kitsubito Linux) | 1× kitsubito (2026-06-16 run 27652755792 attempt 1, v0.8.1 PR #17) | crates.io dep-download blip during the notify-shell build — `download of config.json failed, curl failed` fetching `serde_json`: a transient registry/network fetch failure on the runner, not a code or test fault (a real dep break fails both runners deterministically; this hit one runner once) | observed; cleared by `gh run rerun --failed` (attempt 2 green, builds reused). H...\n*18│| 9 | `spt dummy_harness_e2e` (BOTH tests: `endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches` + `endpoint_run_attach_awaits_online_before_attaching`) | 1× kitsubito (2026-07-05, REMOTE-TRUTH Batch-2 re-gate @f2a799e; roles swap run-to-run) | **process-global `set_var` race in a multi-test e2e binary.** Both tests `std::env::set_var(\"SPT_HOME\", own_tempdir)` then do IN-PROC reads (`perch::spt_home`/`adapters_dir`, `registry::register`, psychebin `fs::copy` staging). One file = one test b...\n*19│| 10 | `spt-store registry::tests::concurrent_registration_never_locks` | 1× hfenduleam local gate (2026-07-05, REMOTE-TRUTH Batch-2 bless matrix @3f6f327, fresh worktree, `nextest --workspace --no-fail-fast`) | 16-concurrent-writer SQLite registration gate FAILED at 5.63s — the SQLITE_BUSY load-contention class ALREADY documented at `db.rs:60`: box saturated by the 1588-test full-parallel suite meant a writer couldn't schedule its retry inside the default 5s `registry_busy_timeout`. Isolated ×5: gre...\n*20│| 11 | `spt-daemon::resume resume_mode_brain_spawning_new_sessions_delivers_each` | 1× kitsubito (2026-07-05 run 28768663321 attempt 1, REMOTE-TRUTH [twohost] PR #52) | Phase-B heavy-class timing EOF — \"session exited before the expected output arrived\" (UnexpectedEof) at 62s under the serialized heavy pool. SAME FAMILY as this file's prior Linux-CI flake (7940318, D4-2b: sequential spawn/read hardening) — a spawned session's exit raced the expected output read on the slow shared box. File untouched...\n 21│| 12 | `spt-daemon::input_ack_deadlock input_flood_through_serve_attach_does_not_deadlock_broker` | 1× hfenduleam local gate (2026-07-06, REMOTE-TRUTH F-2 matrix @a21bc6b, `nextest --workspace --no-fail-fast`) | deadline-margin under full-suite load: the flood exchange's `recv_timeout(30s)` (input_ack_deadlock.rs:512) blew under the 1608-test parallel pool + 7 leaky daemons — FAIL at 34.5s vs ~32s natural runtime isolated (the margin is ~3s by design). Subsystem disjoint from the triggering commit (F-...\n 22│| 13 | `spt::bin cli::tests::probe_all_cap_batches_into_windows` | 1× hfenduleam local gate (2026-07-06, stack matrix @0f08fa9 leg-3 rerun) | fast unit (0.52s natural) FAILED at 1.7s under the 1614-test full-parallel pool — probe-batching window assert with a timing component starved under load; subsystem disjoint from the triggering stack (F-2 reporting / E-2 grid title / picker rider). Isolated ×5: green, 0.52–0.55s. Same night as #12 — the full-parallel local matrix under a busy box is the com...\n 23│| 14 | `spt::bin rc::tests::attach_viewport_reconnects_across_a_broker_bounce` | 1× hfenduleam local gate (2026-07-06, WORKER-TRUTH W-3 build @worker-truth, `nextest -p spt --bin spt` full-parallel 360-test run) | **TIMEOUT, not an assertion fail** — SLOW>60…>TERMINATING at the 240s cap under the 360-way `--bin spt` pool. A REAL-broker + broker-bounce reconnect UNIT test (rc.rs:2071) that escaped the HEAVY-class serialization: the Phase-A/B split + `heavy-broker-pty` test-group target integration te...\n## GATEWAY-LIVENESS-DISPATCH.md\n  7│- **human `spt endpoint list`** → **ONLINE** (renders local perches via `roster::enumerate`, roster.rs:72 = `is_perch_alive` WITH the pid-fallback: status-absent → `is_process_alive(pid)` → alive).\n *8│- **`spt endpoint list --json`** → **Suspended** (renders the endpoint via `resource_projection` over the registry snapshot, cli.rs:2855 → `EndpointRowJson.status` = the registry row status).\n *9│- flynn confirmed mobile-gw IS advertised in all 3 subnet registry files (NOT a re-advertise gap — his earlier \"empty by_id\" was a walker error, retracted).\n 10│\n*11│The adapter's suspend-honor poll reads `--json` (`parse_endpoint_status` over `endpoint list --json --show-all`) → sees Suspended → self-suspends the gateway, despite pid-alive + human-ONLINE.\n 12│\n 13│## Key source facts (already traced)\n*14│- `advertised_status` (registryhost.rs:822-833) DOES feed `effective_rest_state` the pid-fallback `is_perch_alive` (line 825). So the DAEMON's advertised value should be Active for a pid-alive gateway — the divergence is NOT the advertise computation.\n 15│- So the `--json` Suspended comes from the projection/rendering path dropping the pid-fallback the human local-roster applies. **Candidate sites to pin (STEP 1):**\n*16│  1. `resource_projection` (spt-net registry.rs:566) — does it re-derive/filter liveness (e.g. `is_registry_entry_alive`, status-based: liveness.rs:152, which for a status-ABSENT row = not ONLINE||UNBOUND → stale/offline) and DOWNGRADE the row to Suspended/offline, ignoring the advertised Active? (its test `resource_projection_filters_hidden_and_offline` @registry.rs:1821 says it DOES filter on liveness.)\n*17│  2. OR the registry ROW's advertised status is genuinely Active but the `--json` render maps it wrong.\n 18│  3. OR the human path applies a `reconcile_self_owned` local-truth override for locally-owned endpoints that the `--json` path skips (roster.rs:52 mentions reconcile_self_owned overriding stale subnet gossip).\n*19│- **DEFECT A (separate, CONFIRMED, stands):** `establish_perch`'s build (spt/src/api/startup.rs:214-302) carries `cwd/controllable/adapter/read_env` forward from the prior record but NOT `rest_state` → a re-bind writes a fresh InfoJson that WIPES the wake intent (flynn's tick11 rest_state:active vanish).\n 20│- **DEFECT B (RECLASSIFIED to LATENT — do NOT make it the primary fix):** the `status=online` write in `cmd_listen` (startup.rs:486-491) is gated on `live_capable` (manifest `[session.psyche_init]`); a gateway has no psyche_init so it never writes status=online. BUT the pid-fallback is DESIGNED to cover an absent status and DOES for the roster + advertised_status. The operator's ready_agent counterexample proved this: ready_agents work because claude-spt declares psyche_init (live_capable-by-manifest),...\n 21│\n 22│## The fix (after STEP-1 pin)\n   │...\n 24│- Fix at the pinned site (most likely: apply the local-roster / self-owned reconcile to the `--json` local-endpoint rows, OR make `resource_projection`'s liveness for a locally-owned row honor the pid-fallback instead of a status-only check).\n*25│- Do NOT reach for DEFECT B (forcing status=online for gateways) as the fix — it risks the seed-#5 orphan-listener falsely-ONLINE amplification; the reader-parity is the correct, minimal fix. (If STEP-1 shows the cleanest fix genuinely is establishing status for a relay-holding listener, flag it back to me before taking it — that's a design call, not a silent choice.)\n 26│- **DEFECT A:** preserve `prior.rest_state` on re-bind in `establish_perch`'s build (same carry-forward discipline as cwd/controllable).\n 27│\n 28│## REQs (mint at build-start, activate-don't-pre-fail)\n## GATEWAY-RCA-STEP1.md\n  9│```rust\n*10│let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new)); // registry-row status\n 11│... EndpointRowJson { status: format!(\"{:?}\", r.status), ... }             // raw passthrough\n 12│```\n*13│`resource_projection` (spt-net `registry.rs:566`) **passes `instance.status` through\n*14│verbatim** (`registry.rs:592`) and only skips non-routable rows (`:582`). It does\n 15│**NOT** re-derive liveness. So the `--json` subnet-row status IS the persisted\n*16│WAN-snapshot registry-row status.\n 17│\n 18│The human picker path, by contrast, calls **`reconcile_self_owned`**\n 19│(`crates/spt/src/picker/data.rs:160`, via `gather_endpoints` `:112`): for any subnet\n   │...\n 26│(ONLINE/Active); `--json`'s subnet section emits the un-reconciled snapshot status\n*27│(Suspended). The adapter's suspend-honor poll (`parse_endpoint_status` over\n 28│`endpoint list --json --show-all`) reads that subnet-section status → self-suspends.\n 29│\n 30│## Candidate elimination (doyle's 3 STEP-1 candidates)\n 31│- **Candidate 1 (resource_projection re-derives/downgrades liveness) — REFUTED.**\n*32│  `resource_projection` copies `instance.status` (`registry.rs:592`); its only\n 33│  liveness act is `!status.routable() → skip` (`:582`), which would DROP the row,\n 34│  not paint it Suspended. flynn sees the row *present* AND Suspended ⇒ the row's\n 35│  stored status is Suspended and it is routable-enough to render.\n   │...\n 41│\n*42│Note: `advertised_status` (registryhost.rs:822) DOES compute Active via the\n 43│pid-fallback, so the daemon's *live* advertise is Active — consistent with your\n 44│trace. The divergence is that the `--json` reader shows the **persisted snapshot**\n 45│row (which can carry a stale/crash-time Suspended) without the local-owned reconcile\n   │...\n 55│`--json`. Minimal + does NOT touch the advertise or force status=online (avoids the\n*56│seed-#5 orphan-listener false-ONLINE amplification you flagged).\n 57│\n 58│`REQ-HAZARD-BIND-REST-STATE-CARRY` (DEFECT A, confirmed, independent): preserve\n 59│`prior.rest_state` on re-bind in `establish_perch`'s build (spt/src/api/startup.rs\n*60│~214-302), same carry-forward discipline as cwd/controllable/adapter/read_env.\n 61│\n 62│**Awaiting your greenlight on the pin + fix approach before I build (RCA-first).**\n 63│Open Q: should the reconcile live in the cli `--json` builder (surgical, mirrors\n## INJECT-SETTLE-REARM-GATE-VERDICT.md\n  9│\n*10│**Fix:** `settled_once` one-shot → `probe_unobservable` latch + pure `should_settle(attempt, probe_unobservable) = attempt>1 || !probe_unobservable`. Re-settle before EVERY delivery on an OBSERVABLE (echoing/interactive) PTY (bug-prone class, cheap); latch-skip the steady-state settle ONLY where the probe is UNOBSERVABLE (non-echoing ConPTY — no reattach race, full-deadline cost). The settle's own bool return discriminates the class, latched on attempt 1; a re-drive always settles. Mints REQ-HAZARD-I...\n 11│\n 12│## Diff review — FAITHFUL\n*13│Matches the design ruling exactly: latch semantics, pure gate, first-attempt-only latch update, re-drive-always-settles. KH 7.37 + traceable REQ minted with all three stages (`doc`/`impl`/`unit`) tagged on real evidence. RED-first unit `should_settle_rearms_on_observable_pty` present and correct. No int edit — safety-checked: `inject_control_wedge` mocks (`findstr`/`cat`) never answer the DSR probe → settle times out → `probe_unobservable` latches true after delivery 1 → identical to the old one-...\n 14│\n 15│## Both legs GREEN\n 16│- **Windows (ConPTY, isolated worktree + fresh target):** clippy `--workspace --all-targets -D warnings` = 0; `traceable-reqs check --json` = 0; nextest `--workspace` = **1707/1707** (9 leaky, 1 skip), exit 0. `should_settle_rearms_on_observable_pty` PASS.\n   │...\n 19│## GATE-HOLD investigated → CLEARED (env contamination, not code)\n*20│First Windows full-suite run REDed on `inject_control_wedge::wan_arrival_to_idle_spt_hosted_injects_with_no_hook_poll` (`pending_spool_rows: left=1 right=0`, `INJECT_HEAD_LOSS_SPOOL` fired). Discriminated:\n 21│- Deterministic 5/5 isolated FAIL → not a flake.\n*22│- Root: `SPT_INJECT_VERIFY_ECHO=1` was exported in the gater's interactive live-agent shell (the live box arms Layer-2 echo-verify), inherited by the gate script → `echo_capable=true` → on Windows ConPTY the choreo child puts a headless line on the ring → Layer-2 false-positive head-loss → respool → `pending=1`.\n 23│- Proven env-driven: same test PASSES with `SPT_INJECT_VERIFY_ECHO` unset (deterministic both ways). Linux passed because its ssh env is clean.\n 24│- **CI-safe:** the var is **Process-scope only** (User/Machine scope empty) — the GitHub Actions runner service (hfenduleam, separate process tree) does NOT inherit it → PR CI runs clean. The clean-env full re-run (1707/1707) = CI's actual condition.\n 25│- The fix touches only Layer-1 settle; Layer-2 echo-verify is untouched W5-A code. The failure is orthogonal to the change.\n## KNOWN-HAZARDS.md\n   4│\n  *5│**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it...\n   6│\n   7│---\n   8│\n    │...\n  11│### 1.1 Phantom INIT_SIGNOFF after grace period\n *12│- **Failure:** orphan teardown enqueues INIT_SIGNOFF before the grace-period recheck; a transient Self recovery (binary handoff, brief stale poll) makes the recheck pass-as-alive, but the signoff was already spooled and drains on the next iteration → teardown despite a live Self.\n *13│- **Invariant:** grace-period wait MUST complete *before* composing/delivering INIT_SIGNOFF; the recheck must bind `still_gone` before any envelope write.\n  14│- **spt-core mapping:** in-daemon now (no separate wrapper), but the ordering invariant is identical — orphan/teardown logic must re-evaluate liveness after the grace wait, not before enqueue.\n  15│- **Sister cite:** `src/live/wrapper/orphan.rs:201-259` (sleep@209 precedes compose@231-251); tests T-grace-recovery:576, T-still-gone-recheck:618.\n  16│\n    │...\n  19│- **Invariant:** consult liveness via the supervisor (`is_wrapper_alive`-equivalent) before any grace gate; reads of state files must tolerate concurrent writes (atomic write + rename, or read-retry).\n *20│- **spt-core mapping:** the daemon owns both writer and reader → use in-process locking/snapshotting instead of racing on disk. Cross-node registry reads remain eventually-consistent and must tolerate staleness.\n  21│- **Sister cite:** `src/common/list_filter.rs:100-150`; `src/owl/poll.rs:141`.\n  22│\n  23│### 1.3 Stale `index.lock` wedge from prior git crash\n    │...\n  28│\n *29│### 1.4 Deferred spool rows must not leak to the event stream\n *30│- **Failure:** a hook spools a deferred (spool-only, no TCP wake) notice; startup `drain_all` flushes ALL rows including deferred → event emitted at wrong time/priority.\n *31│- **Invariant:** startup drain (and idle/timeout TCP-wake sites) use `drain_non_deferred` only; deferred rows are picked up by their intended consumer via `peek`. All drain sites must agree on which rows they flush.\n *32│- **spt-core mapping:** carries directly — the daemon's spool-drain has the same deferred-vs-immediate distinction.\n *33│- **Sister cite:** `src/owl/poll.rs:276-316`; `spool::drain_non_deferred_with_metadata`.\n  34│\n  35│### 1.5 Worker (working-perch) lifecycle path consistency\n  36│- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.\n  37│- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\n *38│- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\n  39│- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\n  40│\n  41│---\n    │...\n  45│### 2.1 Parent PID over ephemeral poll PID\n *46│- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\n  47│- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\n  48│- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\n  49│- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n    │...\n  71│- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `...\n *72│- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`inf...\n *73│- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let ...\n *74│- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n  75│\n  76│---\n  77│\n    │...\n  86│### 3.2 Stale signoff sentinel must not kill a fresh start\n *87│- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\n *88│- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\n  89│- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\n  90│- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\n  91│\n  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  94│- **Invariant:** on orphan path, synchronously run the echo-commune (final delta) before composing INIT_SIGNOFF; skip only if the session_id is missing.\n  95│- **spt-core mapping:** the daemon runs psyche/pulse loops in-process; ordering invariant identical.\n  96│- **Sister cite:** `src/live/wrapper/orphan.rs:175-199`; tests A-H:333-565.\n    │...\n 106│- **Sister cite:** `src/owl/poll.rs:1-73`; `src/common/envelope.rs`.\n*107│- **CR-linesafety `[REQ-HAZARD-ENVELOPE-CR-LINESAFE]`:** the EVENT is LINE-FRAMED, so the codec must neutralize raw `\\r` too — `event_body_escape` folds CRLF/lone-CR to `\\n` (→`<br>`) **before** framing. **Failure (field, 2026-06-08):** a cross-node `spt send` from Windows (`echo` → CRLF) carried a raw `\\r` into the single-line envelope; the receiver terminal did a CR→column-0 overwrite (`</EVENT>` clobbered `<EVENT t`). `\\r` was never line-representable here, so normalizing it is robustness, not...\n 108│\n 109│### 4.2 Two-slice envelope parser is panic-free and tolerant\n 110│- **Failure:** malformed envelope (unclosed/misordered/nested tags) panics or drops output.\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## MANIFEST.md\n  6│\n *7│**SPT is not a harness.** The manifest declares only what *varies per harness*. Command templates are **opaque strings** — spt-core never parses out a model, tool list, or flag; the adapter writes the full command line and spt-core just runs it with substitution keys filled. Anything spt-core owns (sentinels, spool/registry/perch schemas, the event-block vocabulary, the version-trampoline mechanism, config knobs) is **not** in the manifest.\n  8│\n  9│## What is NOT in the manifest (spt-core-owned)\n 10│\n*11│- **Sentinels** (`.idle`-equiv, echo-gate) — managed by `api state`/`api echo-gate`; the adapter only *calls* them.\n*12│- **Spool / registry / perch `info.json` / daemon-state schemas** — core.\n*13│- **Event-block vocabulary** — the XML tags spt-core surfaces to agents (e.g. `<SPT_EVENT>`, `<owl_messages>`-equivalent) are a **fixed, documented constant**, not configurable. Adapters pass spt-core's output through unchanged.\n 14│- **File-drop filenames** — statically `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md`. Only the *watched directory* is declared (in `[session]`).\n*15│- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.\n*16│- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.\n*17│- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).\n 18│\n 19│<!-- [doc->REQ-MANIFEST-1] The sections below are the authoritative schema; the\n 20│typed form in `crates/spt-runtime/src/manifest.rs` is kept in lockstep. -->\n   │...\n 23│\n*24│### `[adapter]` — header\n 25│```toml\n*26│[adapter]\n 27│name = \"claude-spt\"\n 28│version = \"2.0.0\"\n 29│min_spt_core_version = \"1.0.0\"     # readable before any update; compat gate\n   │...\n 31│shortcut_basename = \"cc\"           # optional; brands the picker's launcher → cc-<id> (default \"spt\")\n*32│host_binaries = [\"claude\"]         # optional; the harness exe(s) this adapter hosts agents inside\n 33│```\n 34│\n*35│<!-- [doc->REQ-ADAPTER-VERSION-CMD] -->\n*36│`version` is **mandatory**. It is the single declared adapter version — read\n*37│before any update (the `gh_release` compare point), surfaced by `spt adapter\n*38│version <name>` (which prints this `[adapter].version` of a registered adapter;\n 39│exit 1 if unregistered), and the value the post-update `message` gate keys on.\n 40│There is no second version source — no `[strings].version`, no `get-string`\n 41│convention.\n 42│\n*43│<!-- [doc->REQ-ADAPTER-FLOOR-ENFORCE] -->\n 44│`min_spt_core_version` is the **enforced** compatibility floor. Both acquisition\n*45│verbs — `spt adapter add` and `spt adapter update` — REFUSE when the installed\n 46│spt-core is below this version, naming the installed core, the floor, and the next\n 47│action (update spt-core first). The check is a numeric per-component compare (so\n 48│`0.9.0 < 0.25.0`), and it fires **before** anything is written: a refused add leaves\n*49│the registry untouched, and a refused update leaves the live install byte-untouched.\n 50│The enforcement is unconditional — there is **no** opt-in flag (the former\n 51│`[update].version_check` knob was retired; it was never read).\n 52│\n   │...\n 56│shortcut at the project root (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic\n*57│`spt` (→ `spt-<id>`); an adapter sets it to brand its shortcuts — `claude-spt`\n 58│uses `cc`, giving `cc-doyle`. spt-core never hardcodes a harness name; the\n*59│picker reads this from the **resolved** manifest of the selected adapter. The\n 60│launcher is the current OS's native form (`.cmd` on Windows — `.ps1` is excluded\n 61│by the default `PATHEXT`; a POSIX `sh` `+chmod +x` on Unix) and overwriting is\n 62│guarded by a generated-by sentinel (a foreign same-named file is never clobbered).\n   │...\n 65│`host_binaries` *(optional, since v0.9.0)* — the harness executable basenames a\n*66│`kind=\"harness\"` adapter hosts agents inside (e.g. `[\"claude\"]`). This is the\n 67│bind-time **match-key**: a harness-hosted session's parent process maps to its exe\n## MSG-IDENTITY-DISPATCH.md\n  8│\n *9│Registry: all wave REQs registered on this branch (`triage/msg-identity-milestone`, off\n 10│main@d73e19e = v0.30.6). Stages are QUEUED — **todlando flips each wave's `required_stages` IN\n 11│the build commit with its evidence tags** ([[traceable-per-wave-activation]]; the triage PR keeps\n 12│`traceable-reqs check` green).\n   │...\n 24│  Root: perri F-036 RCA — leaked `SPT_ENDPOINT_ID=doyle` through psyche_resume spawns rotated\n*25│  doyle's perch to psyche sids every pulse. Adapter half shipped v0.18.8.\n 26│  **Fold:** the leg-b docs fix — `recursion_guard_env` schema description is role-agnostic\n 27│  (manifest.rs:314 + crates/spt-runtime/manifest.schema.json:306, drop \"summarizer children\").\n 28│- `REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD` [impl,unit] — refuse a bind whose `--set-session-id`\n   │...\n 32│### W2 — BRINGUP-BIND (F-034 legs a/b/c; hertz /sptc:live cascade)\n*33│- `REQ-ADAPTER-UNRESOLVED-HINT-FORM` [impl,unit] — hint prints the clap-valid group-level\n*34│  `--adapter` form. Trivial.\n 35│- `REQ-LISTEN-SEED-CONSUME-AFTER-BIND` [doc,impl,unit,int] — validate + BIND first, consume the\n 36│  seed only on success (same effect-before-irreversible-consume class as F-032). Doc stage pins\n 37│  the refusal-consumption contract in harness-contract/api.md.\n   │...\n 47│\n*48│### W4 — DELIVERY-INTEGRITY (dup-delivery cluster; **RCA-first**)\n*49│Instrument exists: REQ-SPOOL-TAKE-AUDIT shipped (spool `taken_leg/taken_sid/taken_at`,\n*50│hidden `spt spool-audit --json`) — use it; do not rebuild it.\n*51│- `REQ-LIVE-AGENT-NO-INJECT-DELIVERY` [impl,unit] (F-033) — root ALREADY code-grounded:\n 52│  `is_spt_hosted_no_relay` (inject.rs:37-49) treats a controllable live_agent with no relay\n 53│  address as inject-target → duplicate + stuck-unsubmitted input garbage. Fix = exclude\n 54│  state:live_agent from the inject predicate (perri confirmed core-only). Buildable immediately.\n 55│- `REQ-CARRIER-CLAIM-EXCLUSIVE` [impl,unit,int] — **RCA-first**: operator-observed default-window\n*56│  send to BUSY doyle delivered via BOTH poll AND relay (spool row 156 `taken_leg='idle-inject'`).\n*57│  Pin with spool-audit: poll-drain vs relay delivered=1 ordering; busy→idle edge re-offer;\n 58│  take-mark atomicity. Then: one atomic cross-carrier claim per row.\n*59│- `REQ-RELAY-NO-BUSY-DELIVER` [impl,unit] — companion: idle-edge drain re-verifies activity /\n 60│  defers to the claim. May REDUCE to an ordering assertion under the exclusive claim — doyle\n 61│  rules at RCA lock; don't build blind. **Do not break** F-023 anti-starvation +\n*62│  REQ-IDLE-PARKED-DELIVERY (already-idle delivery stays green).\n 63│- `REQ-SEND-STAMP-AGENT-ID` [impl,unit,int] — a live session's shelled `spt send` stamps the\n 64│  agent id, not `cli@<node>` (send-time self-resolve via the existing session-pin machinery).\n 65│  Identity leg; independent of the RCA — can build parallel.\n   │...\n 94│- Release: deployah, counter from published metadata, bump rides IN the release PR. CHANGELOG\n*95│  census = field symptoms across all six waves (dup deliveries, seat-theft, lost communes,\n 96│  bringup dead-ends, gateway-down-after-restart, poison log floods).\n 97│\n 98│<!-- Co-authored by: doyle -->\n## MUSL-RELEASE-HANDOFF.md\n 5│## What's gated (don't re-gate the code — verify the PUBLISH)\n*6│- release.yml musl matrix (kitsubito, `CC_x86_64_unknown_linux_musl=musl-gcc`, in-job musl-tools+cmake+target); assemble globs `spt-x86_64-*` into SHA256SUMS + upload; xtask signs + update-set-enters musl by registry-derived triple (zero xtask code change post-W1).\n 7│- **Proven manually on kitsubito:** static-pie musl binary (no glibc), self-IDs `x86_64-unknown-linux-musl`, live fetch correctly rejects `NoArtifactForPlatform(musl)` PRE-publish. Int test pins update-set select+verify-over-musl-bytes.\n 8│\n 9│## Drive the release","missingPaths":["test"],"truncation":{"content":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 2|\n*3|**Status:** **Ratified (2026-06-09).** Independently verified-with-amendments (2026-06-09, agent `doyle` — see §0); six amendments folded in (marked **[V1]**–**[V6]**). The operator ratified both open decisions: the §9 artifacts and the **sequencing** decision (§6.1 — restoration = next milestone, before `spt-claude-code`). The §9 artifacts are now **delivered** (ADR-0018, `REQ-HAZARD-BROKER-PROCESS-ISOLATION` + `REQ-HAZARD-ROLLBACK-STATE-COMPAT`, KNOWN-HAZARDS 6.7/6.8); build plan = `RESTORATI...\n 4|\n 5|**Audience:** dual (human reviewer + AI dev-agent). This is a *why* document, not an implementation plan.\n 6|\n...\n 51|- **broker** (stable kernel) — holds *only* the un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets, and (per the §B ownership table) the Iroh/QUIC endpoint + conn table. Minimal, versioned local IPC. Almost never updates.\n*52|- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk and re-attaches to broker-held handles.\n 53|\n 54|Explicit evidence the split was meant to be a **process** boundary:\n 55|- ADR-0004 Consequences: *\"A small internal broker **process** exists beneath the daemon — a deliberate, bounded walk-back of 'literally one process,' … guaranteeing endpoint survival across updates.\"*\n*56|- ADR-0004 rejected alternatives include **\"whole-daemon live FD-passing\"** and **\"drain + restart\"** — i.e. the chosen design is specifically *restart the brain, the broker process survives.*\n 57|- `M3a-PLAN.md`, `M3-PLAN.md`, `M3b-PLAN.md` all name \"the broker **process** + IPC (M3b).\"\n 58|- Spikes 01/03/04/05/06 each **PROVEN** with *two separate binaries* (`spt-spikes/spike-01-broker-handoff`): a PTY child + a live QUIC transfer survive 100× brain restarts, gapless and exactly-once. A spike that proves \"survives a brain restart\" presupposes the broker is a separate process.\n 59|\n...\n 156|\n*157|**Decision.** The broker tracks each session-subscriber's last-delivered cursor. On (re)start the new brain queries the broker for **all** hosted sessions and re-attaches each in **resume** mode. **Output is at-least-once** (the broker resumes from last-*sent*); **input/effects stay exactly-once** via the already-broker-owned `EffectJournal`. The explicit `BrainState` handoff *message* is **retired** for production — the new brain cold-starts and reconstructs continuity by querying the broker.\n 158|\n 159|**[V2] Generation custody.** Retiring `BrainState` would orphan the generation counter (KH 2.4: increments on every start/revive; `gen_start` always `= now()`). Custody moves to the **broker**: it owns the counter, increments it on **every** brain spawn (planned or crash), and hands `{generation, start-reason}` to the brain at spawn time via a **versioned argv/hello field** (KH-2.3 forward-compat, defaulted). Because the broker observes every respawn, this is strictly more reliable than passing it brain-...\n 160|\n...\n 162|- Two concrete gaps exist today: `applyhost.rs:239` re-attaches only `sessions.first()` (one session) and with `from_seq = 0` (re-replays the whole ring → duplicate output).\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...\n 199|\n*200|This is **milestone-sized**, not a patch. It includes: the process decomposition; an IPC-verb surface for the hard cases (digest push, seed-lock/liveness, plus the general direct-call audit — net bring-up is near-free per the §Q5 audit correction); broker-side per-session delivered-cursor tracking; broker-owned generation custody handed at spawn **[V2]**; durable absolute-deadline timing across **phase-significant** resident loops only **[V4]** (the idempotent pump cadences need *no* conversion — co...\n 201|\n 202|### 6.1 Sequencing — RATIFIED (operator, 2026-06-09)\n 203|\n...\n 206|- **(a) Queue is open** — mesh shipped at v0.3.0, M8 acceptance is COMPLETE.\n*207|- **(b) Self-bootstrapping (decisive).** Until this ships, *every* release needs manual daemon bounces fleet-wide — paid on all 3 nodes for v0.3.2, and `enlyzeam` reproduced the `\\r`-corruption for ~a day with the fix sitting on disk. The restoration release is the **last** one that needs a manual bounce; once it lands, every adapter-era release rolls seamlessly.\n*208|- **(c) Adapter window safe.** The split changes daemon *internals*, not the CLI/api surface M8 froze. Better the adapter lands on the final topology than atop a daemon we later perform open-heart surgery on while it hosts the user's daily driver.\n 209|\n 210|Ratified by the operator 2026-06-09.\n 211|\n...\n 215|\n*216|- **Alarms have no durable daemon scheduler.** In the spt-core daemon, `alarm` exists only as an event *shape* (`spt-proto`) + relay handling (`psyrelay.rs:93`) + a test fixture; `target-time` is only a wire attribute. The actual \"wait until target, fire the TIMED PULSE\" timer lives in the legacy owl listener in-memory. Porting alarms into the daemon as a durable scheduler (which would then ride the Q4 one-shot mechanism) is its own gap.\n*217|- **Registry one-cadence reconverge window.** `RegistryHost` is intentionally in-memory (`registryhost.rs:19-26`): a restart forgets peers' rows and reconverges within one pump cadence (self-rows re-advertised each tick; mirrored to `identity/registry/` JSON for out-of-process readers). Across a brain restart this is a brief reconverge window, accepted by design — not a blocker, noted for completeness.\n*218|- **Multipart inter-agent message truncation — belongs to LEGACY `claude_skill_owl`, not spt-core.** During the verification exchange (which ran over the legacy live-agent messaging infra — the `owl.exe` listener, not the spt-core daemon), an 18-part chunked message reached the recipient as ~parts 1–8; a long resend also dropped its tail. Long messages silently lose their tail — a delivery-integrity issue in the chunk-reassembly path. **This is a legacy-project bug** (the agents in this exchange ...\n 219|- **Stale DEFERRED mesh row.** Side-flag from verification: the mesh row in `docs/DEFERRED.md` is stale (mesh shipped at v0.3.0). Minor cleanup, noted for a docs pass.\n 220|\n 221|---\n...\n 228|- **Spikes** — `docs/spikes/SPIKE-01-broker-handoff.md`, `-03-quic-survival`, `-04-forkpty-parity`, `-05-restart-stress`, `-06-idempotent-boundary` (proofs that the two-process handoff works on both OSes).\n*229|- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.\n 230|\n 231|---\n 232|\n*233|## 9. Artifacts — DELIVERED (ratified 2026-06-09)\n 234|\n 235|Verification is done (§0); the operator ratified; these durable artifacts are now written (commit 44497f4 unless noted):\n 236|\n 237|- **ADR-0018** (`docs/adr/0018-broker-brain-process-isolation-restoration.md`) — extends + amends ADR-0004: the drift finding, the Q2–Q8 restoration decisions, the six verification amendments. Status: Accepted (2026-06-09).\n*238|- **`REQ-*` registry mints** (registry-first, `traceable-reqs.toml`; minted INACTIVE per rule 5, activated per `RESTORATION-PLAN.md`'s schedule):\n 239|  - **`REQ-HAZARD-BROKER-PROCESS-ISOLATION`** — a brain restart must never drop a hosted endpoint (REQ-UPD-3's real promise). **Evidence [V5]:** an `int` stage productionizing SPIKE-01/03 — a PTY child + a live QUIC conn survive a brain-*process* restart onto a *swapped* binary. REQ-UPD-3 / REQ-DAEMON-2's current `int` evidence (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`) proves only the *in-process* handoff shape — **mis-evidenced today, re-pointed at plan task D7.**\n 240|  - **`REQ-HAZARD-ROLLBACK-STATE-COMPAT` [V1]** — a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable).\n 241|  - The functional two-process-split realization + the **[V6]** N-1 old-broker × new-brain CI compat test are carried as `RESTORATION-PLAN.md` tasks D1–D7 (the compat test lands as a D7 gate).\n## CONTEXT-MEMORY.md\n 40|\n*41|The live/project authoring directives are **core-owned** — fed to every Psyche by spt-core (part of the composed psyche-download the daemon writes to the turn's `{psyche_context_file}`), so the split is consistent across all adapters (inconsistent rules → sync chaos). Adapters add only harness mechanics (how to write the drop file).\n 42|\n 43|## memformat (deeply integrated, self-evolving, the split scaffold)\n 44|\n...\n 69|\n*70|`Concepts`/`Aliases` make context **retrievable**; `Applies to` enables fine-grained scoping + query-routing; `Source` carries provenance; `Pinned` drives start-injection density (below).\n 71|\n 72|## Retrieval Map (generated INDEX)\n 73|\n*74|A generated, lightweight **INDEX** over topics — summaries + concepts + aliases + paths — **not** a vector DB, **not** a full mirror (adapted from pi-agent-memory's Retrieval Map). One INDEX per **project** (aggregating all agents' project context → realizes cross-agent synthesis + query-routing: \"which endpoint owns this task\") and per **agent**. Regenerated on context change; the merge driver + INDEX regeneration keep it current.\n 75|\n 76|## Start-injection: dense working set, sparse tail\n 77|\n## DEBUG-CONVERGE-PLAN.md\n 32|  convergence table, exit codes, loopback + two-host tests.\n*33|- **Out (still deferred):** remote build orchestration, dashboards/TUI, adapter\n 34|  inclusion, stable-release convergence. Keep the first watcher narrow.\n 35|\n 36|## CLI\n...\n 53|trust-derived subnet membership introduced in `b9326f4` (self + pinned peers,\n*54|registry rows as enrichment) so a freshly-joined or eviction-decayed node is\n 55|still *expected* and shows up as a row, not a silent omission. `--nodes` gives\n 56|an explicit set for targeted tests.\n 57|\n...\n 87|   the responder's identity for free.\n*88|2. **Registry/presence enrichment.** If a node already publishes its applied\n*89|   version + provenance into a registry row (the rollout metadata carries\n 90|   `git_commit`/version provenance), the coordinator can read convergence from\n 91|   rows it already syncs — no new round-trip. Verify whether\n*92|   `relcache`/registry persists \"applied version\" anywhere queryable; if not,\n 93|   prefer option 1.\n 94|\n 95|Pick option 1 as the baseline (self-contained, testable on loopback); treat\n## DEFERRED.md\n 7|| Scrollback on-disk spillover | terminal wrapper v1 | In-memory ring covers the common case; spillover adds a persistence/rotation story | First long-running session that overflows the ring usefully, or any \"scroll back further than the buffer\" user need |\n*8|| Sidecar adapter process (long-running, wire-protocol) | harness contract v1 | Manifest + `spt.exe` subcommand surface covers v1 harnesses; sidecar only earns its keep for streaming / in-memory cross-event state | A harness outgrows manifest+hooks (needs streaming or stateful coordination) |\n 9|| Manifest `include` / composition (option C) | manifest v1 | Single flat manifest per harness is enough until a harness wants to override only part of a section | First sub-project that needs to inherit spt-core defaults but override one section |\n 10|| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |\n*11|| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one...\n 12|| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |\n 13|| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant s...\n 14|| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |\n*15|| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |\n*16|| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |\n 17|| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |\n*18|| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |\n 19|| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the ...\n 20|| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concr...\n*21|| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect,...\n 22|| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-u...\n 23|| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the \"no connection\" state in `subnet status` + the coming-online banner...\n 24|| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8...\n*25|| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruni...\n*26|| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n*27|| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite ho...\n 28|| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\n 29|| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache...\n 30|| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n*31|| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`w...\n*32|| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version ...\n 33|| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n 34|| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer \"serving subnet X?\", a...\n 35|| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures...\n...\n 40|| **On-disk `current_exe` reconcile after auto-rollback (the D6-2 reboot residual)**: readiness-gated auto-rollback (ADR-0018 Q7, restoration D6-2) is **record-driven selection** — after a rollback the broker's supervisor keeps spawning the last-known-good `.old-N` binary, chosen from the durable `RolledBack` record, so the rescue survives a reboot for free **with no file rename at the failure instant** (the brittle path the design rejected). But the bytes on disk at `current_exe` are STILL the quarant...\n*41|| **Quarantine operator override (`spt update apply --force` — the note-i escape hatch)**: D6-2's quarantine guard refuses re-applying a version that auto-rolled-back on this node (the `RolledBack` record is the marker; clears naturally when N+1 stages). But a version whose failure was **environmental** (a transient boot condition, not bad bytes) leaves the node **permanently allergic** to a genuinely-good version with no escape but staging a newer one. FIX = an explicit operator override (`spt update ...\n 42|| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers m...\n*43|| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's \"remind me at 3pm\" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today...\n 44|| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **aut...\n*45|| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n 46|| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47|| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n*48|| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n*49|| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n*50|| **Always-on endpoint kind + `#`-sigil addressing (ADR-0023; REQ-EP-8 / REQ-EP-9) — the core seam for adapter-bridged group/forum messaging**: a `[always-on]` manifest section declaring a resident, owner-less, addressable endpoint binary the daemon boot-launches + supervises **continuously** (independent of agent liveness, register-triggered, one per `<adapter>[:profile]`), which self-manages its `#`-addressed channel endpoints via the existing `api bind` and may call the already-ungated `endpoint wak...\n## DOCS-STRATEGY.md\n 30|- **Human dev:** two agents exchange a message end-to-end in **one command + minimal config**, < 10 min — the core primitive, whole-thing-first, then decomposed.\n*31|- **Dev-agent integrator:** a minimal **adapter** that satisfies the manifest + `api` contract and drives one lifecycle operation — the \"build a harness for spt-core\" hello-world.\n 32|\n 33|## Agent-consumable docs (spt-core specifics)\n 34|\n...\n 38|- **Markdown content negotiation** + deterministic include/exclude tags so agent exports carry the canonical path and drop human-only narrative.\n*39|- **Machine-readable manifest/contract schema** (JSON Schema) at a stable, discoverable path — agents validate their adapter manifests against it; the schema *is* documentation.\n 40|- **MCP doc/resource server** — the standout fit, given the harness contract is the core integration surface. Expose the contract + reference as **deterministic, side-effect-free MCP resources** (same request → same result), giving integrating agents structured knowledge without RAG fuzziness. Highest-leverage agent-docs move for spt-core.\n 41|- **CLI help as first-class agent docs** — agents primarily shell `spt api …`, so `spt <cmd> --help` must be complete, exampled, and machine-readable (a `--json`/structured help mode). The CLI surface is a documentation surface.\n 42|\n...\n 44|\n*45|Deployed to GitHub Pages on `spt-releases` (ADR-0014; truth + CI generation stay in the source repo). **Tier 1 (ships with v0.1):** mental model/overview · human quickstart (install → two agents message) · adapter quickstart (via `mock-adapter`) · **harness contract vertical complete** (public manifest reference + full `spt api` reference + install-on-demand bootstrap pattern) · shell adapter getting-started (`spt-shell-notify` as the live example) · CLI reference generated from clap, CI-gated · ...\n 46|\n 47|**Site generator: mdBook + custom theme CSS** (Rust-toolchain-coherent — one `cargo install mdbook` on the existing runners; the Rust Book IA this strategy copies *is* mdBook). Styling north star: **Astro Starlight** — copy its look/feel in the theme CSS, not its toolchain. Raw `.md` published alongside rendered pages (`/x.html` ↔ `/x.md`) for the agent-export convention; `llms.txt` / `llms-full.txt` / `manifest.schema.json` as static assets at site root.\n 48|\n...\n 82|- [Quickstart: two agents exchange a message](…/quickstart/messaging.md): one command, <10 min.\n*83|- [Quickstart: build an adapter](…/quickstart/adapter.md): minimal manifest + `api` contract.\n 84|- [Mental model](…/concepts/overview.md): daemon, endpoints, instances, subnet.\n 85|\n 86|## Capabilities      (one bullet per vertical, each → its own llms.txt)\n...\n 101|# Quickstart — two agents exchange a message, end to end, <10 min.\n*102|> Pick your path: **developer** (here) · **building an adapter / dev-agent** → /quickstart/adapter.md\n 103|\n 104|## 1. Install (one line)        curl -fsSL https://spt-core.dev/install.sh | sh\n 105|   Verify:                      spt --version      # spt 0.1.0 (proto v1)\n...\n 119|0 Getting started → 1 Quickstart (the early runnable project) → 2 Mental model\n*120|→ 3 Capability verticals (build-order) → 4 Capstone: build a non-CC adapter end-to-end\n 121|```\n 122|**Vertical ordering (dependency-sequenced):**\n 123|`Messaging → Lifecycle → Terminal → Networking → Harness contract → Instances → Shells → Self-update`\n...\n 140|/// The manifest's `min_spt_core_version` exceeds this binary.\n*141|/// **Fix:** update spt-core, or pin the adapter to a compatible release.\n 142|VersionTooOld { required: Version, found: Version },\n 143|```\n 144|\n## DORMANCY-BUDGET.md\n 5|**What this answers.** ADR-0003's Stage-A red-team #9: *\"dormant-warm default\n*6|has real resource/security cost — measure N dormant sessions on real adapters;\n*7|define an adapter-specific warm/cold policy + resource budget before locking\n 8|warm as the default.\"* This document records the measurement and locks the\n 9|policy with numbers instead of a guess.\n 10|\n...\n 36|Knobs: `SPT_BUDGET_N` (seats), `SPT_BUDGET_PROGRAM`/`SPT_BUDGET_ARGS` (the\n*37|seat command — point at the real adapter's harness), `SPT_BUDGET_SETTLE_MS`,\n 38|`SPT_BUDGET_IDLE_MS`. Rig runs ride the CI `[budget]` commit-message gate\n 39|(both hosts measure in one tagged push) or a direct local\n 40|`SPT_BUDGET=1 cargo test -p spt-daemon --test budget --release -- --nocapture`.\n...\n 53|\n*54|3 × `claude` (real adapter seat — the heavy case):\n 55|\n 56|| metric | per-seat |\n 57||---|---|\n...\n 69|5 × `sh` (shell seat — no LLM harness installed on this host, so the shell\n*70|floor is its measurement; the adapter-class number generalizes from\n 71|HFENDULEAM's, the harness binary being the same node runtime there):\n 72|\n 73|| metric | per-seat |\n...\n 84|1. **Warm (dormant) stays the default.** An idle warm seat burns **zero CPU**\n*85|   on both adapter classes; the cost is purely resident memory. A shell-class\n 86|   seat is ~8 MiB — effectively free at any realistic seat count. A heavy\n 87|   LLM-harness seat is ~300 MiB — a workstation-class node holds ten warm\n 88|   seats in ~3 GB, acceptable for the instant-reactivation win\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n 10|seam) are spt-core changes** (todlando's wave, sequenced after F-029). **The\n*11|trust-seeder binary itself is adapter-side** (perri's wave — spt-core stays\n*12|harness-agnostic per the glue-model boundary: adapters = manifest + binary only;\n 13|no harness-aware source in spt-core).\n 14|\n 15|---\n...\n 43|| 2 | ACCOUNT-ROOT-MISMATCH — trust seeded in a DIFFERENT config root than the spawn resolves; wedges despite a valid-looking seed | Reavo/Brandon dual-root box (si1 vs b6b probes) | SILENT |\n*44|| 3 | PSYCHE-CRASHLOOP — nested psyche CC dies at trust dialog; wrapper respawned ~3/s, ordinal 5358 in 30 min | F-h; now guarded by adapter v0.13.0 give-up + core C3(b) | SILENT (guarded) |\n*45|| 4 | ADAPTER-UNRESOLVED — no registered adapter hosts the parent binary | seed→listen path, bash parent | LOUD pre-spawn (good shape) |\n 46|| 5 | STALE-CLI-CONTRACT — bringup recipe passes a removed flag; \"unexpected argument\"; spawn never happens | old recipe on 0.22.0+ | LOUD pre-spawn (good shape) |\n 47|| 6 | RELAUNCH-NEVER-BINDS — 2nd `endpoint run` same id after tree-kill; prints but never binds | **0.22.0 only — FIXED on 0.23.0 by F-028 B1/B4** (perri definitive repro 2026-07-03: tree-kill → same-id re-run re-binds fresh session, live PTY, honest info.json; first check was rig-void, --subnet omitted, caught before reporting) | FIXED-NOTE (not in F-027 scope) |\n 48|| — | UAC-740 elevation spawn-fail | NOT observed (subnet-op elevation only) | out of scope |\n...\n 93|  deadline separates cleanly at ≥30 s; **never configure below 30 s**),\n*94|  overridable per-adapter via a manifest key (the bringup-time contract\n*95|  belongs to the adapter) and per-invocation via `--bind-deadline <secs>`\n 96|  (the per-node escape for cold-broker first-spawn / loaded boxes). Escape\n 97|  hatch `--no-wait` restores fire-and-forget and prints\n 98|  `ENDPOINT_RUN_DETACHED:{id}` (explicitly unverified — never `_STARTED`).\n...\n 105|\n*106|### Half B — pre-spawn manifest seam (spt-core executes; adapter owns the binary)\n 107|\n 108|New manifest key **`pre_spawn`** inside `[session.<role>]` tables (applies to\n 109|`self` AND `resume` — field-confirmed necessary on resume: a resume is a\n...\n 115|1. **Env parity via capture-at-creation (BINDING; sharpened by perri review):**\n*116|   env vars the adapter declares `[env.<NAME>] direction=\"read\"` are **captured\n 117|   at endpoint creation and stamped into the perch record**; the broker threads\n 118|   the IDENTICAL stamped values to BOTH the pre-spawn command AND the session\n 119|   spawn. Neither consumer reads its own process env for a stamped var — if the\n...\n 126|   `[env]` declares, knowing nothing of `CLAUDE_CONFIG_DIR` specifically.\n*127|   (Field-confirmed adapter shape: claude-spt declares\n 128|   `[env.CLAUDE_CONFIG_DIR] direction=\"read\"` with `~/.claude` fallback —\n*129|   adapter reads, never sets.)\n 130|2. **Install-dir-first resolution (REQ-INSTALL-11):** the program path resolves\n*131|   via `resolve_program_in_dir` like every other adapter-manifest program.\n 132|3. **Bounded + loud:** runtime capped (30 s); non-zero exit or timeout ABORTS\n 133|   the session spawn with `ENDPOINT_SPAWN_FAIL:{id}: reason=PRE_SPAWN_FAIL:\n 134|   <exit/detail>` on CLI stderr (relayed through the spawn RPC error — this\n...\n 163|  owlery cwd is trust-seeded by the same pre-spawn seam, and the crashloop\n*164|  backstop (C3(b) + adapter give-up) already guards the symptom.\n 165|- No mode-6 work: FIXED on 0.23.0 by F-028 B1/B4 (perri definitive repro).\n 166|  The Half-A deadline token still catches its shape if it ever regresses.\n 167|\n...\n 174|| 3 PSYCHE-CRASHLOOP | Half B (owlery cwd seed) | existing C3(b)/v0.13.0 guard |\n*175|| 4 ADAPTER-UNRESOLVED | — (already good) | existing pre-spawn error |\n 176|| 5 STALE-CLI-CONTRACT | — (already good) | existing / Half A `EXITED_PRE_BIND` |\n 177|| 6 RELAUNCH-NEVER-BINDS | FIXED (F-028 B1/B4, 0.23.0-verified) | Half A `NO_SESSION`/`WEDGED_PRE_BIND` (regression net only) |\n 178|\n...\n 184|  (never binds) → assert today's silent `ENDPOINT_RUN_STARTED` becomes the\n*185|  token. Real-daemon int candidate (mock-adapter seam exists — F-028 pattern).\n 186|- `REQ-PRESPAWN-COMMAND-SEAM` — manifest `pre_spawn` executes with spawn-env\n 187|  parity + install-dir-first resolution + bounded runtime; failure aborts\n 188|  loud. Red-first unit on env parity (seeded sentinel var visible to pre-spawn\n...\n 191|  bug, perri 2026-07-03): the conformance test IS the Half-A red-first int.\n*192|- Adapter-side (perri's repo, NOT a core REQ): trust-seeder contract —\n 193|  forward-slash exact key, root-as-spawn-resolves, idempotent.\n 194|\n 195|## 6. Docs surfaces (docs-drift gate applies)\n...\n 218|   the session spawn consume the stamp — neither reads its own process env.\n*219|   Adapter shape confirmed: `[env.CLAUDE_CONFIG_DIR] direction=\"read\"`,\n*220|   fallback `~/.claude`, adapter reads and never sets.\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n 3|Status: **READY FOR BUILD 2026-07-04 — wave called by operator.** Author: doyle.\n*4|Inputs: perri `PSYCHE-EPHEMERAL-PLAN.md` (adapter half, doyle-gated PASS; built on\n 5|parked branch `psyche-ephemeral-parked`, publish held for this core wave), the\n 6|2026-07-04 field case (both directions — v0.13.2 bad-ship + hall-bf multi-subnet\n 7|poll refusal, evidence table §2), doyle code recon (anchors verified @f8428a8).\n...\n 10|spt-core** (todlando's wave). **The psyche role binaries/templates are\n*11|adapter-side** (perri's parked branch — publishes ONLY after the core floor\n 12|releases; see §7 sequencing, a BINDING lesson from the v0.13.2 bad ship).\n 13|\n 14|---\n...\n 35|\n*36|**(a) The adapter cannot HOLD residency.** The resident runner's per-pulse\n 37|liveness poll (`spt ready <id> --once`) hard-refuses on a multi-subnet home\n 38|(`READY_FAIL: … pass --subnet`). The nested psyche process **cannot know its\n 39|subnet/home**: core fills only `base_keys` = `{id}`, `{session_id}`, `{node}`\n...\n 42|the daemon re-hosted, ~5 s churn forever: hall-bf-psyche ordinal 6491+, at\n*43|one point 54 stamped host attempts. Adapter v0.13.4 made the death LOUD\n 44|(`PSYCHE_POLL_FAIL`), but residency remains structurally impossible there.\n 45|\n 46|**(b) The daemon EXPECTS residency, and that expectation is destructive.**\n*47|Adapter v0.13.2 accidentally shipped the parked ephemeral shim: its by-design\n 48|exit after the seed turn tripped the residency machinery, which tore down the\n 49|endpoint's hosted state — ready marker removed, never re-stamped, every\n 50|force-native gated `leg=cli-gate-not-hosted` **permanently** (field brick,\n...\n 58|- **`.live-bin` own-copy:** exists solely so the long-lived resident's image\n*59|  doesn't Windows-image-lock the adapter registry dir during `spt adapter\n 60|  update` (the crc-swap `.old`-litter hazard class). Ephemeral turns hold the\n 61|  image only for a bounded turn — the own-copy machinery loses its reason.\n 62|- Psyche TURNS already work daemon-driven today (`psyche_download`, revival\n...\n 68||---|----------|-------|-----------|\n*69|| 1 | `LIVEHOST_PSYCHE_NONRESIDENT` xN in EVERY iso-home run, both adapter versions | perri RC + wakefin rigs | daemon expects residency |\n*70|| 2 | hall-bf-psyche boot-churn ~5 s cadence, ordinal 6491+, synthetic `sess-<pid>-<ts>` ids, no real session ever binds; starts exactly at BIGNET join (7/3) | operator box, real home | adapter can't hold residency |\n*71|| 3 | Poll-verb root repro: `spt ready <id> --once` on 2-subnet home → exit 1, `READY_FAIL … pass --subnet`, empty stdout | perri, clean repro | adapter can't hold residency |\n 72|| 4 | v0.13.2 shim-exit → residency teardown → ready removed → permanent `leg=cli-gate-not-hosted` | hall-bf + perri wake-confirm | residency expectation destructive |\n 73|| 5 | Psyche turns land fine via daemon-driven paths while the resident churns | hall-bf digest (`psyche_download` at every boundary) | per-event shape already proven |\n 74|| 6 | ~12/min re-host cadence NEVER tripped the C3(b) thrash guard (boot records are apparently not ledger boundaries to the guard) | hall-bf | guard blindness (rides this wave) |\n...\n 160|\n*161|Remove the own-copy spawn path for psyche roles (spawn from the registry\n*162|install dir like every other bounded adapter invocation — image-lock exposure\n 163|is now turn-bounded, same class as `translate`/`digest` today). GC existing\n 164|`owlery/*/.live-bin/` dirs (displace+GC pattern from F-028 C1 if any is\n 165|image-locked at update time). Compat: a manifest that still names the\n...\n 185|\n*186|- No adapter-aware code in core: core wires WHEN turns run and WHAT keys they\n*187|  get; the adapter's parked branch owns what a psyche turn does (glue-model\n 188|  boundary holds).\n 189|- No relay-unregister fix (the post-clear `owl send → NO_PERCH` casualty) —\n 190|  separate seed, C-2 follow-up (a), field-repro'd 2026-07-04, sequenced with\n...\n 205|| Thrash-guard blindness (12/min invisible) | W3 ledger-counted budget | red-first synthetic loop trips it |\n*206|| `.live-bin` image-lock class | W5 (bounded turns from registry dir) | existing crc-swap displace+GC net |\n 207|| Reseed loop (custody lost repeatedly) | W2 reseed + W1 budget | `PSYCHE_RESEED` marker + budget stamp |\n 208|\n 209|## 5. Requirement seeds (register at wave start, `required_stages=[]` until the chunk activates)\n...\n 216|  survives the turn.\n*217|- `REQ-PSYCHE-SID-CUSTODY` — psyche templates receive the psyche's OWN sid;\n 218|  parent boundary does not rotate it; reseed is loud. Red-first: parent\n 219|  `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's\n 220|  — guard-revert reproduces).\n...\n 227|  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,\n*228|  failing psyche → parent stays deliverable, no rehost churn, error stamped.\n 229|- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the\n 230|  failure budget counts real attempts (ledger-derived); a 12/min failure loop\n 231|  trips it. Red-first synthetic loop.\n*232|- Adapter-side (perri's repo, NOT core REQs): parked-branch ephemeral roles;\n 233|  `min_spt_core` floor = the release that ships this wave; publish gated on\n 234|  that floor being LIVE (bad-ship lesson, binding).\n 235|\n...\n 244|  **single-node multi-subnet rig** (the hall-bf shape: 2 subnets, one home).\n*245|- **Adapter (perri):** `psyche-ephemeral-parked` branch rebased once core API\n 246|  shape freezes (she reviews W1/W2 key contract before her rebase — field\n 247|  review slot like F-027 §7); publish ONLY after core floor releases.\n 248|- **hall-bf interim:** stays turn-functional + visibly stamped (v0.13.4) until\n## F-035-CONFLICT.md\n 3|**Raised by:** todlando, mid-build, 2026-07-09. **Build HELD** (WIP uncommitted on\n*4|`f-035-window-drain-honor`). doyle's COLLAPSE ruling collides with a **shipped**\n 5|acceptance gate my RCA (docs/F-035-RCA.md) missed. Need an A/B/C ruling before I proceed.\n 6|\n 7|---\n...\n 13|\n*14|**`crates/spt/tests/idle_edge_drain_e2e.rs:89`** — `fn spool_while_active_then_idle_fires_injection`\n*15|`// [int->REQ-MSG-IDLE-EDGE-DRAIN] [int->REQ-HAZARD-DELIVERY-STARVATION]`\n 16|\n*17|- **Setup:** spools `spool_message_deferred_at(perch, \"peer\", …)` — which is\n*18|  `insert_message(…, WINDOW_ACTIVE_ONLY, …)` (spool.rs:363). So the row is\n 19|  **`active_only`**.\n 20|- **Trigger:** `spt api state idle <id>` — the real ACTIVE→IDLE edge.\n 21|- **Assertion (the keystone):** `assert_eq!(pending_after, 0, …)` — the `active_only`\n*22|  row **MUST be delivered** on the idle edge (spool cleared).\n 23|\n*24|My fix → the `active_only` row is excluded from the idle claim → stays spooled →\n 25|`pending_after == 1` → **this shipped gate goes RED.**\n 26|\n*27|`REQ-MSG-IDLE-EDGE-DRAIN` (F-023 leg 2) and `REQ-SEND-WINDOW-DRAIN-HONOR` (F-035) are\n 28|directly incompatible for the relay-less spt-hosted class. The active/idle asymmetry I\n 29|called a bug in the RCA is **intentional** for that class.\n 30|\n...\n 35|For an **spt-hosted** endpoint (broker owns the PTY, `controllable`, relay-less), the\n*36|idle-edge/parked drain is the **ONLY** carrier for an `active_only` row. Traced every path:\n 37|\n 38|| carrier | `active_only` to spt-hosted? |\n 39||---|---|\n*40|| Send-time inject (`cli.rs:5716` `try_inject = prefer_native \\|\\| !active_only`) | **NO** — cli.rs:5713 verbatim: *\"a non-native `--active-only` is the explicit hook channel: **spool-only, no inject attempt**.\"* |\n 41|| Send-time idle re-offer (`cli.rs:5734`) | **NO** — gated `&& !active_only`. |\n*42|| Hook poll `drain_active_window` (`api poll`) | **N/A** — that is the HARNESS-hosted hook channel; an spt-hosted endpoint has no `api poll` harness. |\n*43|| **Idle-edge / parked drain `drain_idle_spool`** | **YES** — the F-023 leg-2 behavior (`include_deferred = !resting`). **This is the only carrier.** |\n 44|\n 45|So: `active_only` is designed as *\"the active **hook** window\"* — meant for\n 46|**harness-hosted** endpoints that `api poll`. An **spt-hosted** endpoint has no hook, so\n...\n 59|  `active_only`+spt-hosted is knowingly revoked (that class starves, or gets an\n*60|  evaporate/refuse-at-send policy instead). Requires re-scoping `REQ-MSG-IDLE-EDGE-DRAIN`\n 61|  + rewriting its int.\n 62|- **(B) never-starve wins** — the idle edge *should* carry `active_only` to spt-hosted\n 63|  (F-023 correct). Then **F-035 is mis-scoped**: *\"never wakes\"* is the **harness** hook\n...\n 65|  never a contract for the hook-less spt-hosted class. **RETIRE or NARROW\n*66|  REQ-SEND-WINDOW-DRAIN-HONOR; no code change.**\n*67|- **(C) class split** — spt-hosted delivers `active_only` at idle **without waking**\n 68|  (non-interrupting background inject) vs harness-hosted holds-for-poll. New behavior,\n*69|  larger; needs a \"deliver-but-don't-wake\" inject mode the translation binary honors.\n 70|\n 71|**todlando lean:** the carrier table points at **(B)** — the field-doc *\"never wakes an\n 72|idle target\"* (cli.rs:85) describes the harness hook model, and F-023 already reasoned\n...\n 74|`--deferred` background row reaching an idle spt-hosted agent) may be **working as\n*75|designed** — it is the only way that class can ever be delivered. If the true concern is\n*76|*\"it interrupts,\"* that is **(C)**, not the drain-exclusion of **(A)**. But this is your call.\n 77|\n*78|My WIP (spool.rs window-axis collapse + inject.rs + 2 units) is staged **uncommitted** — I\n*79|touched neither delivery.rs nor worker.rs. Standing by for A / B / C.\n## F-035-RCA.md\n*1|<!-- [doc->REQ-SEND-WINDOW-DRAIN-HONOR] F-035 RCA — report-before-fix. -->\n*2|# F-035 RCA — idle-edge parked-drain ignores the delivery-window tag\n 3|\n 4|**Status:** RCA-first, **report before fixing** (doyle-dispatched 2026-07-09).\n*5|**REQ:** `REQ-SEND-WINDOW-DRAIN-HONOR` (registered, `required_stages = []` — activate on doyle's fix ruling).\n 6|**Investigator:** todlando. **Off:** `origin/main@2c05dc9` (v0.30.2).\n 7|\n 8|---\n...\n 20|\n*21|For an **spt-hosted, relay-less** endpoint (the class the idle-edge drain targets),\n*22|such a row is delivered through the translation binary at the idle window — the\n*23|exact delivery the tag forbids.\n 24|\n 25|---\n 26|\n...\n 28|\n*29|The two delivery-window surfaces are **asymmetric**. One honors the `window` axis;\n 30|the other keys on the legacy `deferred` mirror and **never consults `window`**.\n 31|\n*32|### The delivery-window model (ADR-0028 W2, `spt-store/src/spool.rs`)\n 33|\n 34|| window        | active hook poll | idle / wake |","truncated":true,"truncatedBy":"bytes","totalLines":853,"totalBytes":98176,"outputLines":458,"outputBytes":51180,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":853,"totalBytes":98176,"outputLines":458,"outputBytes":51180,"shownRange":{"start":1,"end":458},"nextOffset":459}}},"isError":false,"timestamp":1784197376864}}
{"type":"message","id":"a67770a3","parentId":"362f40b3","timestamp":"2026-07-16T10:22:56.878Z","message":{"role":"toolResult","toolCallId":"call_Bz8DCw1gYLSjR3hbxSUGf8zU|fc_017bff223290b7ba016a58b100c19c819aae9219b27c97ef52","toolName":"grep","content":[{"type":"text","text":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 34|\n*35|1. The hot-swap that *is* wired into `apply` is a **per-session brain handoff** (`apply_brain_only` → `Brain::handoff`), whose job is endpoint survival (REQ-UPD-3), not running-process replacement.\n*36|2. By explicit design, `applyhost.rs:36-38` states: *\"The daemon's own resident loops keep running the old logic until its next restart (`ensure_running` spawns the executable path — now the new binary); that restart is routine lifecycle, never an endpoint cycle.\"*\n 37|3. So new code only goes live on a *subsequent, unrelated* daemon restart (next logon, or a manual restart — which is what made the *other* node, `hfenduleam`, show a new pid). The practical consequence on `enlyzeam`: `gerald`'s live daemon kept running 0.3.0 — including the message-envelope codec — so the box from the original bug report **was still reproducing the original `\\r` corruption even though the fix sat on disk.**\n 38|\n 39|The operator then asked the real question this document answers: **what would it take to make an update actually hand off to the new executable seamlessly — no dropped processes, internal loop timings consistent across the swap, plus any other angles that matter?**\n...\n 50|\n*51|- **broker** (stable kernel) — holds *only* the un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets, and (per the §B ownership table) the Iroh/QUIC endpoint + conn table. Minimal, versioned local IPC. Almost never updates.\n 52|- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk and re-attaches to broker-held handles.\n 53|\n 54|Explicit evidence the split was meant to be a **process** boundary:\n*55|- ADR-0004 Consequences: *\"A small internal broker **process** exists beneath the daemon — a deliberate, bounded walk-back of 'literally one process,' … guaranteeing endpoint survival across updates.\"*\n 56|- ADR-0004 rejected alternatives include **\"whole-daemon live FD-passing\"** and **\"drain + restart\"** — i.e. the chosen design is specifically *restart the brain, the broker process survives.*\n 57|- `M3a-PLAN.md`, `M3-PLAN.md`, `M3b-PLAN.md` all name \"the broker **process** + IPC (M3b).\"\n 58|- Spikes 01/03/04/05/06 each **PROVEN** with *two separate binaries* (`spt-spikes/spike-01-broker-handoff`): a PTY child + a live QUIC transfer survive 100× brain restarts, gapless and exactly-once. A spike that proves \"survives a brain restart\" presupposes the broker is a separate process.\n...\n 72|\n*73|- The brain cannot restart onto a new binary without killing the in-process broker thread — which would close every PTY, orphan every harness child, and drop every listening socket. So **the no-endpoint-drop self-update pillar is silently unrealized.**\n 74|- `apply` therefore performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — achieving nothing for a binary swap. `update.rs:234`'s statement that \"the live daemon execs the verified new binary\" is **aspirational; it was never wired** because the process split it depends on does not exist in production.\n 75|- Updates do not run until an unrelated restart/logon (observed live, Section 1).\n 76|\n...\n 78|\n*79|> **Glossary note already applied:** `CONTEXT.md` was sharpened during the session to state explicitly that there is **one broker per machine** (per `SPT_HOME`), *not* one per endpoint — a single broker hosts every endpoint's resources and is present even with zero endpoints online. This corrected a latent \"one broker per endpoint\" misconception and is load-bearing for decision Q2.\n 80|\n 81|---\n 82|\n...\n 86|\n*87|1. no endpoint or harness child process is dropped or suspended (REQ-UPD-3),\n 88|2. internal loop timings stay consistent across the swap,\n 89|3. a failed update fails *safe*,\n 90|4. the mechanism is uniform across Windows and Linux.\n...\n 101|\n*102|**Decision.** The broker process is the long-lived per-machine anchor. It owns the **seed-control lock + liveness** (the single-daemon-per-home invariant and the `ensure_running`/`is_running` ping target) and **supervises/spawns the brain** as its child. On update, the broker re-spawns the brain.\n 103|\n*104|**Reason.** The broker is the one component guaranteed present whenever the machine's daemon is alive — it runs even with zero endpoints (the \"bare daemon\" case, `daemon.rs:196`). The seed lock and liveness *must* survive a brain restart, or a second daemon could win the bind during the restart window and `is_running` would flap. Anchoring them in the never-restarting layer makes the single-instance invariant and liveness survive brain restarts for free.\n 105|\n 106|**Alternatives rejected.**\n 107|- *A thin third supervisor process spawning both broker and brain* — adds a process for no invariant the broker can't already hold.\n*108|- *Brain as parent, spawning the broker* — incoherent: a brain restart would orphan or kill its own broker child, dropping endpoints.\n 109|\n*110|**Operator's challenge that strengthened this.** The operator initially held a \"one broker per endpoint\" model and correctly objected that liveness shouldn't live in something that only exists when an endpoint is online. The code shows one broker *per machine*, always present, so the objection's principle is satisfied and actually reinforces putting liveness in the broker.\n 111|\n 112|### Q3 — Update trigger: brain self-exit, broker auto-respawn from disk\n 113|\n...\n 125|- **Periodic resident loops** (pump cadences, pulse/echo-commune): persist a `(anchor, interval)` pair **once** on a fresh/crash start. The next fire is derived functionally — `next_fire = anchor + interval × ⌈max(0, now − anchor) / interval⌉` — with **no per-fire writes**. An **update restart re-reads `(anchor, interval)` and keeps deriving** (phase preserved, lands mid-grid). A **crash restart is treated as a fresh start** (rewrite `anchor = now`; phase reset is acceptable).\n*126|- **One-shot scheduled events** (alarms): persist the absolute `target-time` **at creation**; every start (update *or* crash) reads it and fires-if-due. **These never reset on crash** — a user's \"remind me at 3pm\" is a commitment that must outlive any restart. **[V3]** The one-shot *rule* is specified this milestone, but the *machinery* is **built with the alarm port, not here**: the daemon has no one-shot consumer today (§7), so building it now would ship untested dead code (matches the project's act...\n 127|\n 128|The brain distinguishes update-restart from crash/fresh-restart via a signal on the broker's respawn (the broker *initiated* the planned cycle but *observed* the unexpected crash). The `(anchor, interval)` write happens once per fresh/crash start, zero per update, zero per fire.\n 129|\n...\n 169|\n*170|**Decision.** If the new brain fails to reach a **ready** signal (re-attached all sessions + resumed loops) within a bounded number of boots / a healthy-run window, the broker **rolls back to the last-known-good binary** (the `spt.exe.old-N` backup), **quarantines** the bad version (no auto re-apply/re-fetch), and surfaces a loud consent-style notification. The broker holds **last-known-good + candidate** paths and chooses which to spawn (no file manipulation at failure time). Endpoints survive throughou...\n 171|\n 172|**[V1] Rollback-state-compat invariant (forward invariant, mint this milestone).** Auto-rollback spawns the **old** binary against durable state the **new** brain may already have written. This is safe *today* (a source audit confirmed zero state-migration code exists), but the first release that migrates a durable-state schema would silently break Q7's rollback. The invariant must be minted now, while it is free: **a brain must not irreversibly migrate durable state before ready-promotion** — equivale...\n 173|\n 174|**Reason.**\n*175|- The dangerous case is a *runtime* bug (new brain panics on boot), not the binary land (already rolled back at `applyhost.rs:167-170`) and not IPC mismatch (the `classify` handshake `brain_ipc_version ≥ broker.min_compatible` is checked pre-swap). A naive supervisor would crashloop the new binary: endpoints alive but logic dead — \"up but useless.\"\n 176|- The backup binary is already produced for exactly this, and the readiness+backoff machinery already exists — `peerloop.rs:82-91` has `SUPERVISE_BACKOFF_BASE/CAP` + `SUPERVISE_HEALTHY_RUN` (60 s healthy resets backoff), and `supervise_pump` is wired at `peerloop.rs:805` (M8-D4, verified). A bad update that bricks *logic* should self-heal back to last-good without a human, the same spirit as the boot-race self-heal already shipped.\n 177|- The two-phase record fixes a real existing bug: `applyhost.rs:176` writes `record_applied(version)` + `last-outcome=applied` **before** the brain handoff — i.e. before we know the new brain boots. (This is the optimistic `applied.json={version:6}` observed on `enlyzeam`.)\n 178|\n...\n 192|- A new hidden `spt daemon brain` entry is the **brain** process: connects to the broker, runs pump/dispatch/psyche loops/session drivers, rehydrates from disk, emits a `ready` signal.\n*193|- `ensure_running` / `is_running` / `daemon stop` contracts are unchanged — they target the broker's seed channel.\n 194|- The de-elevation guard (`daemon.rs:60-103`) applies at the **broker** entry; the brain child inherits the unelevated identity.\n 195|\n 196|---\n...\n 199|\n*200|This is **milestone-sized**, not a patch. It includes: the process decomposition; an IPC-verb surface for the hard cases (digest push, seed-lock/liveness, plus the general direct-call audit — net bring-up is near-free per the §Q5 audit correction); broker-side per-session delivered-cursor tracking; broker-owned generation custody handed at spawn **[V2]**; durable absolute-deadline timing across **phase-significant** resident loops only **[V4]** (the idempotent pump cadences need *no* conversion — co...\n 201|\n 202|### 6.1 Sequencing — RATIFIED (operator, 2026-06-09)\n 203|\n...\n 215|\n*216|- **Alarms have no durable daemon scheduler.** In the spt-core daemon, `alarm` exists only as an event *shape* (`spt-proto`) + relay handling (`psyrelay.rs:93`) + a test fixture; `target-time` is only a wire attribute. The actual \"wait until target, fire the TIMED PULSE\" timer lives in the legacy owl listener in-memory. Porting alarms into the daemon as a durable scheduler (which would then ride the Q4 one-shot mechanism) is its own gap.\n 217|- **Registry one-cadence reconverge window.** `RegistryHost` is intentionally in-memory (`registryhost.rs:19-26`): a restart forgets peers' rows and reconverges within one pump cadence (self-rows re-advertised each tick; mirrored to `identity/registry/` JSON for out-of-process readers). Across a brain restart this is a brief reconverge window, accepted by design — not a blocker, noted for completeness.\n 218|- **Multipart inter-agent message truncation — belongs to LEGACY `claude_skill_owl`, not spt-core.** During the verification exchange (which ran over the legacy live-agent messaging infra — the `owl.exe` listener, not the spt-core daemon), an 18-part chunked message reached the recipient as ~parts 1–8; a long resend also dropped its tail. Long messages silently lose their tail — a delivery-integrity issue in the chunk-reassembly path. **This is a legacy-project bug** (the agents in this exchange ...\n 219|- **Stale DEFERRED mesh row.** Side-flag from verification: the mesh row in `docs/DEFERRED.md` is stale (mesh shipped at v0.3.0). Minor cleanup, noted for a docs pass.\n...\n 225|- **ADR-0004** — `docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md` (the decision being restored; §A update classes, §B ownership table, the spike amendments).\n*226|- **Requirements** — REQ-UPD-3 (no-endpoint-drop, brain-only), REQ-UPD-4 (staged + consent), REQ-UPD-6/8 (update-set / platform-safe), REQ-DAEMON-3/9 (auto-start / net self-heal), REQ-HAZARD-UPDATE-ROLLBACK, REQ-HAZARD-HANDOFF-ARGV-COMPAT.\n 227|- **Glossary/model** — `CONTEXT.md` daemon / broker / brain (incl. the one-broker-per-machine clause added this session).\n 228|- **Spikes** — `docs/spikes/SPIKE-01-broker-handoff.md`, `-03-quic-survival`, `-04-forkpty-parity`, `-05-restart-stress`, `-06-idempotent-boundary` (proofs that the two-process handoff works on both OSes).\n 229|- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.\n...\n 238|- **`REQ-*` registry mints** (registry-first, `traceable-reqs.toml`; minted INACTIVE per rule 5, activated per `RESTORATION-PLAN.md`'s schedule):\n*239|  - **`REQ-HAZARD-BROKER-PROCESS-ISOLATION`** — a brain restart must never drop a hosted endpoint (REQ-UPD-3's real promise). **Evidence [V5]:** an `int` stage productionizing SPIKE-01/03 — a PTY child + a live QUIC conn survive a brain-*process* restart onto a *swapped* binary. REQ-UPD-3 / REQ-DAEMON-2's current `int` evidence (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`) proves only the *in-process* handoff shape — **mis-evidenced today, re-pointed at plan task D7.**\n 240|  - **`REQ-HAZARD-ROLLBACK-STATE-COMPAT` [V1]** — a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable).\n 241|  - The functional two-process-split realization + the **[V6]** N-1 old-broker × new-brain CI compat test are carried as `RESTORATION-PLAN.md` tasks D1–D7 (the compat test lands as a D7 gate).\n 242|- **KNOWN-HAZARDS** §6.7 (in-process collapse) + §6.8 (rollback-state-compat), with condensed-checklist rows.\n## CONTEXT-MEMORY.md\n 6|\n*7|- **live context** (per-agent, project-independent — the agent's *mind*): identity/persona, relationship + rapport with the user, the user's cross-project preferences & working style, cross-project life threads, **learnings from work that may apply to other projects**, a thin **project roster** (one line per project the agent works on), the agent's self-model. Synced to **all** instances of the endpoint.\n 8|- **project context** (per-agent-per-project — what the agent is doing *here*): codebase/architecture, decisions made here, **current focus / next steps / todos**, project conventions, build/test commands, blockers. Synced **only** to same-project instances.\n 9|\n 10|(This split independently matches pi-agent-memory's Project Store vs Global Store and its discriminator dialogue — convergent validation.)\n...\n 30|- **`/compact`** → resume Self → **commune** file-drop.\n*31|- **`spt suspend`** → resume Self → **signoff** file-drop (suspend = the endpoint going down, so it's a signoff, not a commune).\n 32|\n 33|This deliberately swaps the project-centric echo for a Self-authored capture at exactly the moments that matter (context loss + suspend), so live context stays current. (Routine cadence communes remain echo — project-primary, live-conservative.)\n 34|\n...\n 36|\n*37|A lever for an agent behind an **spt-hosted** endpoint to clear and resume itself without stalling. It: (1) triggers a `/clear` (which uses the **Self-resume-commune rule** above — consistent with all other boundaries), (2) thereby captures a commune, and (3) **guarantees a resume signal hits the harness post-clear** so the agent's turn restarts and it resumes work — starting from its immediate next-steps. Spt-hosted-only because the guarantee requires the daemon to own the PTY and inject the post-cl...\n 38|\n 39|### Ownership\n 40|\n...\n 73|\n*74|A generated, lightweight **INDEX** over topics — summaries + concepts + aliases + paths — **not** a vector DB, **not** a full mirror (adapted from pi-agent-memory's Retrieval Map). One INDEX per **project** (aggregating all agents' project context → realizes cross-agent synthesis + query-routing: \"which endpoint owns this task\") and per **agent**. Regenerated on context change; the merge driver + INDEX regeneration keep it current.\n 75|\n 76|## Start-injection: dense working set, sparse tail\n 77|\n...\n 82|- **Hot/cold curated by the Psyche** via `Pinned` + recency; keeping the hot set tight is part of maintaining memformat.\n*83|- **Tunable** via a working-set **density knob** (spt-core global setting + per-endpoint override).\n 84|\n 85|## Deferred\n 86|\n## DEBUG-CONVERGE-PLAN.md\n 19|After a coordinator stages a debug rollout, answer one question without\n*20|hand-walking nodes: **did every expected lab node converge on the target debug\n 21|version, and if not, why not?**\n 22|\n 23|`debug-converge` watches until every *expected* debug-pinned reachable node\n*24|reports the target version applied, or it returns a per-node timeout table.\n 25|This is what turns \"fast to stage\" into \"fast to *verify deployed*\" — the\n 26|actual deployment-speed win the rollout pipeline exists for.\n 27|\n...\n 43||---|---|---|\n*44|| `--version <u64>` | required | target debug version to converge on |\n 45|| `--nodes <id,…>` | derived | explicit expected-node set; overrides derivation |\n 46|| `--subnet <name>` | the rollout's lab subnet | scope derivation to one subnet |\n 47|| `--timeout <secs>` | 120 | overall watch budget |\n...\n 55|still *expected* and shows up as a row, not a silent omission. `--nodes` gives\n*56|an explicit set for targeted tests.\n 57|\n 58|## Per-node state model\n 59|\n...\n 65||---|---|---|\n*66|| `Applied` | node reports `current debug version == target` | ✓ success |\n 67|| `NotPinned` | node is on the `stable` channel — rejected at the metadata gate | ✓ (expected-set bug or intentional) |\n 68|| `Offline` | unreachable within the round | pending → timeout |\n 69|| `StagedAwaitingConsent` | candidate staged, default-gated, no apply ack yet | pending |\n*70|| `BlockedByBrokerResources` | broker-touching candidate refused while live endpoints held | pending (operator must quiesce) |\n 71|| `Rejected{reason}` | typed refusal: `NoArtifactForPlatform`, `WrongChannel`, `Rollback`, `ArtifactMismatch`, … | ✓ failure |\n 72|\n 73|Convergence = every expected node is `Applied`. Any node still pending when the\n...\n 102|```\n*103|DEBUG_CONVERGE target=7 channel=debug subnet=lab expected=3\n 104|  node-a   Applied                v7\n 105|  node-b   StagedAwaitingConsent  v6  (run: spt update apply)\n 106|  node-c   Rejected               NoArtifactForPlatform (x86_64-apple-darwin)\n## DEBUG-ROLLOUT.md\n 15|- A rollout is a signed `SignedUpdateSet`: one signed metadata record, with one\n*16|  artifact digest per Rust target triple.\n 17|- Each recipient verifies the set under its node-local `release-keys.json`,\n 18|  selects only its own platform artifact, stages it, and then follows the normal\n 19|  consent/apply policy.\n...\n 25|- A broker-touching candidate may be staged, but apply refuses it while\n*26|  broker-held runtime is live. Shut down or suspend hosted endpoints first; the\n 27|  first flow does not auto-cycle them.\n 28|\n 29|## One-time lab setup\n...\n 63|- `--build-current` builds and includes the coordinator's current platform.\n*64|- `--artifact <target=path>` adds an already-built artifact for another target,\n 65|  such as `x86_64-pc-windows-msvc` or `x86_64-unknown-linux-gnu`.\n 66|- `--version <u64>` overrides the local debug sequence. Use a higher value when\n 67|  recovering from lost local state.\n...\n 69|- `--state <path>` stores the local debug sequence somewhere other than\n*70|  `target/debug-rollout-state.json`.\n 71|\n 72|The staged update then propagates through the normal pull-based update pump.\n 73|There is intentionally no bespoke push path.\n...\n 96|`Applied` / `StagedAwaitingConsent` / `NotPinned` / `Rejected{reason}` /\n*97|`Offline` — exiting `0` only when every node applied the target (`1`\n 98|otherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\n 99|The manual walk below remains the fallback.\n 100|\n...\n 103|- `NoArtifactForPlatform` means the signed set did not include the recipient's\n*104|  Rust target triple.\n 105|- `WrongChannel` means the node is not pinned to `debug`.\n 106|- `Rollback` means the offered debug version is not greater than the node's\n 107|  current debug-channel version.\n 108|- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held\n*109|  resources; quiesce hosted endpoints first.\n 110|\n 111|## Agent checklist\n 112|\n## DEFERRED.md\n 10|| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |\n*11|| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one...\n 12|| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |\n 13|| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant s...\n 14|| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |\n 15|| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |\n*16|| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |\n 17|| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |\n*18|| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |\n 19|| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the ...\n 20|| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concr...\n 21|| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect,...\n...\n 26|| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n*27|| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite ho...\n*28|| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\n 29|| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache...\n 30|| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n*31|| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`w...\n*32|| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version ...\n 33|| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n*34|| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer \"serving subnet X?\", a...\n 35|| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures...\n*36|| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual \"Apply and observe\" (`DE...\n 37|| **Origin-source update bootstrap (`spt update fetch` — pull the latest signed release from GitHub into the existing verify→stage→apply pipeline)**: today update DISCOVERY is **peer-only** (REQ-UPD-1: a daemon pulls offers from roster peers via `request_update`; `propagate.rs`/`peerloop.rs`). There is NO origin fetch, so the **first node in a fleet — or any isolated/solo node — cannot get a new release without a peer that already staged it** (the fleet converges by gossip, but only *after* a m...\n 38|| **Test broker socket-bind hardening (CI-flake removal)**: `applyhost` tests' `served_broker` helper does `Broker::bind(name).expect(\"bind broker\")` (`crates/spt-daemon/src/applyhost.rs:293`) where `name` = `spt-daemon-d7b-{pid}-{seq}.sock`. Flakes intermittently on the **kitsubito** self-hosted Linux runner (likely AF_UNIX path collision / stale socket file under the runner's reused workspace — Windows named pipes don't hit it). Cost the v0.3.2 release a full red CI run + a `--failed` rerun (2026-06-...\n 39|| **`spt update fetch` single→set shape-upgrade at same version (REQ-UPD-8 transitional trap)**: `cmd_update_fetch` sets the rollback floor = `staged_version` (the monotonic counter), and the gate is a plain `candidate > floor`. A node that fetched+staged version N as a platform **single** (pre-0.3.2 fetch behavior) can never re-fetch version N as a multi-platform **set** — same counter → `Rollback{current:N, candidate:N}`. Bites the **seed-node bootstrap**: the first node to origin-`fetch` a relea...\n...\n 42|| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers m...\n*43|| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's \"remind me at 3pm\" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today...\n*44|| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **aut...\n 45|| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n*46|| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47|| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n 48|| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n*49|| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n*50|| **Always-on endpoint kind + `#`-sigil addressing (ADR-0023; REQ-EP-8 / REQ-EP-9) — the core seam for adapter-bridged group/forum messaging**: a `[always-on]` manifest section declaring a resident, owner-less, addressable endpoint binary the daemon boot-launches + supervises **continuously** (independent of agent liveness, register-triggered, one per `<adapter>[:profile]`), which self-manages its `#`-addressed channel endpoints via the existing `api bind` and may call the already-ungated `endpoint wak...\n## DOCS-STRATEGY.md\n 27|\n*28|## Killer quickstart targets (two, one per audience)\n 29|\n 30|- **Human dev:** two agents exchange a message end-to-end in **one command + minimal config**, < 10 min — the core primitive, whole-thing-first, then decomposed.\n 31|- **Dev-agent integrator:** a minimal **adapter** that satisfies the manifest + `api` contract and drives one lifecycle operation — the \"build a harness for spt-core\" hello-world.\n...\n 83|- [Quickstart: build an adapter](…/quickstart/adapter.md): minimal manifest + `api` contract.\n*84|- [Mental model](…/concepts/overview.md): daemon, endpoints, instances, subnet.\n 85|\n 86|## Capabilities      (one bullet per vertical, each → its own llms.txt)\n 87|- [Messaging](…/messaging/llms.txt): typed ops + file blobs; send / ring / ready.\n## DORMANCY-BUDGET.md\n 25|   per-seat overhead: PTY plumbing + output rings);\n*26|5. kills every seat (the **suspended** state — only the on-disk perch record\n 27|   would remain in production) and samples the residual.\n 28|\n 29|Sampling: Windows via `Get-Process` (WorkingSet64 / HandleCount /\n...\n 63|residual: host process returns to its ~6–7 MiB baseline — a suspended seat\n*64|costs **only the on-disk perch record** (bytes, not megabytes, no handles, no\n 65|CPU).\n 66|\n 67|### gravity (Ubuntu 22.04, 2026-06-04, `[budget]`-tagged CI run @ 785002e)\n...\n 91|   amended. The knob chain shipped in D9-2 (`auto_suspend_after_ms`: global\n*92|   default OFF → `daemon.json` node leg → `info.json` endpoint leg, `0` =\n*93|   explicit endpoint OFF) is the policy surface.\n 94|3. **Constrained nodes should default the node leg ON.** ~300 MiB/seat is\n 95|   real on a Pi/handheld-class node: set `auto_suspend_after_ms` in\n 96|   `daemon.json` there. The threshold counts from **dormancy onset** (the\n 97|   `dormant_since_ms` anchor), never from last activity.\n*98|4. **Heavy-harness endpoints may opt themselves out/in per seat** via the\n*99|   endpoint override — e.g. a billable harness whose warm session holds a\n*100|   cost/footprint beyond RSS (seat-count limits) sets a per-endpoint\n 101|   threshold even on a big node. Note: an **idle** claude seat holds no API\n 102|   spend (tokens flow only on input); the footprint concern there is seat\n 103|   count and memory, not per-minute billing.\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n*1|# F-027 — ENDPOINT_SPAWN_FAIL: pre-spawn seam + bind-deadline honesty (DESIGN)\n 2|\n 3|Status: **FROZEN 2026-07-03 — field review complete.** Author: doyle. Inputs:\n 4|perri field seeds + spawn-fail taxonomy (2026-07-03, 16-part dispatch,\n...\n 18|\n*19|`spt endpoint run <id> --start` prints `ENDPOINT_RUN_STARTED` when the broker\n 20|has accepted the spawn RPC and a child PID exists — **nothing more is verified**\n 21|(cli.rs:1819 machine line, cli.rs:1833 `--start` success print). A harness that\n 22|launches but wedges post-spawn (workspace-trust prompt, null stdin) never fires\n*23|its SessionStart hook, so `establish_perch` never runs (api/startup.rs:299 is\n*24|the ONLY writer of the `ready` marker), and the endpoint sits `ready=false`\n 25|**forever, silently** — while the operator was told it started.\n 26|\n 27|`ready=false` is already rendered by `spt list`/picker (roster.rs:69 →\n 28|cli.rs:2735/3826) but nothing gates or alarms on it: passive display only. The\n*29|attach path's `await_endpoint_session` (cli.rs:1854, 25 s) gates on broker\n 30|**session existence** — created synchronously at spawn, so it passes instantly\n 31|for a wedged harness — and the `--start` path skips even that.\n 32|\n...\n 35|KNOWN-HAZARDS F-019 lesson). Any F-027 token must land on **CLI stderr and/or\n*36|the perch record**, never daemon-stderr-only.\n 37|\n 38|## 2. Field evidence (perri, 2026-07-03 — provenance table)\n 39|\n...\n 41||---|------|----------|-------|\n*42|| 1 | TRUST-WEDGE — CC at workspace-trust dialog, null stdin; `ENDPOINT_RUN_STARTED` printed; ready=false forever | every fresh probe cwd, this session | SILENT |\n 43|| 2 | ACCOUNT-ROOT-MISMATCH — trust seeded in a DIFFERENT config root than the spawn resolves; wedges despite a valid-looking seed | Reavo/Brandon dual-root box (si1 vs b6b probes) | SILENT |\n 44|| 3 | PSYCHE-CRASHLOOP — nested psyche CC dies at trust dialog; wrapper respawned ~3/s, ordinal 5358 in 30 min | F-h; now guarded by adapter v0.13.0 give-up + core C3(b) | SILENT (guarded) |\n 45|| 4 | ADAPTER-UNRESOLVED — no registered adapter hosts the parent binary | seed→listen path, bash parent | LOUD pre-spawn (good shape) |\n 46|| 5 | STALE-CLI-CONTRACT — bringup recipe passes a removed flag; \"unexpected argument\"; spawn never happens | old recipe on 0.22.0+ | LOUD pre-spawn (good shape) |\n*47|| 6 | RELAUNCH-NEVER-BINDS — 2nd `endpoint run` same id after tree-kill; prints but never binds | **0.22.0 only — FIXED on 0.23.0 by F-028 B1/B4** (perri definitive repro 2026-07-03: tree-kill → same-id re-run re-binds fresh session, live PTY, honest info.json; first check was rig-void, --subnet omitted, caught before reporting) | FIXED-NOTE (not in F-027 scope) |\n 48|| — | UAC-740 elevation spawn-fail | NOT observed (subnet-op elevation only) | out of scope |\n 49|\n 50|Seed-1 correction (accepted): the \"backslash-keyed `hasTrustDialogAccepted`\n...\n 65|\n*66|Target invariant (headline): **\"started\" means BOUND, or the failure is LOUD.**\n 67|The silent class (modes 1, 2, 3, 6) must fail with the same loudness as the\n 68|pre-spawn class (modes 4, 5) already does.\n 69|\n...\n 71|\n*72|After the spawn RPC returns in `cmd_endpoint_run --start`, the CLI **awaits\n 73|bind** — the appearance of the `ready` marker (`resolve_ready_file`, the\n*74|`establish_perch` effect) — bounded by a deadline:\n 75|\n*76|- **Success:** `ENDPOINT_RUN_STARTED:{id}` moves to AFTER the bind is observed\n*77|  — its plain meaning finally true. The early machine line `ENDPOINT_RUN:{id}\n 78|  … pid={pid}` stays at spawn-accept (scripts parse it; unchanged).\n*79|- **Deadline exceeded:** emit **`ENDPOINT_SPAWN_FAIL:{id}: reason=<CLASS>:\n*80|  <detail>`** on CLI stderr, exit non-zero, and **stamp the perch record**\n 81|  (`spawn_fail` field via the locked `mutate_info` RMW) so `spt list` and the\n 82|  picker render the honest state. A later successful bind clears the stamp\n*83|  (`establish_perch` already rewrites state; clearing rides there).\n 84|- Classification probes (all CLI-observable; no daemon change needed to\n 85|  detect): child pid alive? broker `has_session` still true? ready marker\n 86|  present? →\n...\n 97|  hatch `--no-wait` restores fire-and-forget and prints\n*98|  `ENDPOINT_RUN_DETACHED:{id}` (explicitly unverified — never `_STARTED`).\n*99|- The attach (non-`--start`) path upgrades the same way: `await_endpoint_session`\n 100|  keeps its session gate, then the same bind-await runs before declaring health.\n 101|\n 102|Failure of the await machinery itself (CLI killed mid-wait) fails open to\n...\n 116|   env vars the adapter declares `[env.<NAME>] direction=\"read\"` are **captured\n*117|   at endpoint creation and stamped into the perch record**; the broker threads\n 118|   the IDENTICAL stamped values to BOTH the pre-spawn command AND the session\n 119|   spawn. Neither consumer reads its own process env for a stamped var — if the\n 120|   seeder read its own env while the spawn read the stamp (or vice versa),\n...\n 132|3. **Bounded + loud:** runtime capped (30 s); non-zero exit or timeout ABORTS\n*133|   the session spawn with `ENDPOINT_SPAWN_FAIL:{id}: reason=PRE_SPAWN_FAIL:\n 134|   <exit/detail>` on CLI stderr (relayed through the spawn RPC error — this\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n 17|\n*18|The daemon hosts each live endpoint's Psyche as a **long-lived resident\n 19|process** and enforces that residency:\n 20|\n 21|- `livehost.rs:497 → host_one:518` spawns the psyche\n...\n 48|exit after the seed turn tripped the residency machinery, which tore down the\n*49|endpoint's hosted state — ready marker removed, never re-stamped, every\n 50|force-native gated `leg=cli-gate-not-hosted` **permanently** (field brick,\n 51|operator-hit on hall-bf, contained same-day by v0.13.3).\n 52|\n...\n 77|\n*78|Target invariant (headline): **the Psyche is a per-event turn, not a process.\n 79|Liveness of the psyche = \"turns succeed\", never \"a pid is resident.\"**\n 80|\n 81|### W1 — wire the per-event driver (retire the resident spawn)\n...\n 95|The daemon drives every invocation, so every substitution key is filled from\n*96|**daemon-known context** — the child never self-resolves home/subnet/perch.\n 97|Direction-(a) failures become impossible by construction, not by fixing the\n 98|poll.\n 99|\n...\n 108|The psyche mints and keeps **its own session id**, stored in the nested\n*109|`{id}-psyche` perch record — `{session_id}` in psyche role templates becomes\n 110|**the psyche's sid**, never the parent's (today's fill at livehost.rs:518 is\n 111|the parent's — the custody bug). Rules:\n 112|\n...\n 131|  resident child. REQ-HAZARD-LIVEHOST-NONRESIDENT's **spirit** (a psyche that\n*132|  cannot work must be visible on the perch, and hosting must not churn)\n 133|  transfers to the W1 failure budget; the REQ's traceable entry gets a\n 134|  SUPERSEDED pointer to the new REQ (same pattern as LIVENESS-DECAY →\n 135|  SUPERSEDED in C-1).\n 136|- **The teardown that made direction (b) destructive is deleted**: no psyche\n*137|  state may ever remove the PARENT endpoint's ready marker or hosted state.\n 138|  Psyche trouble stamps psyche fields only. (This is the conformance test's\n 139|  core assertion.)\n 140|- C3(b) guard rework rides here (evidence #6): the failure budget counts\n*141|  **attempts via the psyche perch ledger** (boot records / turn records), not\n 142|  whatever it counted that let 12/min churn run invisibly. Red-first: a\n 143|  12/min synthetic failure loop MUST trip the budget.\n 144|\n...\n 146|\n*147|Even fully ephemeral, child-side verbs acting on nested perches must resolve\n 148|without a `--subnet` the child cannot know. Fix in core, general form:\n 149|**nested `{parent}/{nested}` ids resolve under the parent's home** —\n*150|subnet-less resolution for nested perches (`resolve_perch_path`/ready-verb\n 151|family). Additionally expose a single `{subnet}` base_keys fill **when\n 152|known** (lifecycle.rs:338; absent → LOUD missing-key fail, the `{node}`\n 153|precedent on :344). **There is deliberately NO `{home}` key** (doyle W4 Q1: the\n*154|endpoint home subnet is one concept, and `home:` is already the subnet-name\n 155|qualifier position — a `{home}` template key would only invite meaning-drift).\n 156|Red-first = evidence #3's exact repro: 2-subnet home, nested-id verb, must\n 157|succeed.\n...\n 188|  boundary holds).\n*189|- No relay-unregister fix (the post-clear `owl send → NO_PERCH` casualty) —\n 190|  separate seed, C-2 follow-up (a), field-repro'd 2026-07-04, sequenced with\n 191|  the operator's v0.24.0 bug batch, likely same milestone but its own item.\n 192|- No wake-arc work — closed end-to-end 2026-07-04 (five layers, wakefin3 +\n...\n 212|  bounded per-event turn through resume/turn/relay; no resident psyche process\n*213|  exists between events. Red-first: fire an event on a hosted live endpoint →\n 214|  assert one turn ran (side-effect proof file — transcript-based asserts are\n 215|  structurally blind, 2026-07-04 rig lesson) and no `{id}-psyche` process\n 216|  survives the turn.\n...\n 218|  parent boundary does not rotate it; reseed is loud. Red-first: parent\n*219|  `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's\n 220|  — guard-revert reproduces).\n*221|- `REQ-PSYCHE-NESTED-RESOLUTION` — nested-perch verbs resolve under the parent\n 222|  home without `--subnet` on a multi-subnet node; a single `{subnet}` fill\n 223|  present-or-loud (no `{home}` key). Red-first: evidence #3 repro (2-subnet home rig).\n 224|- `REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION` — hazard (paid-for, hall-bf +\n 225|  0.13.2): psyche failure of ANY shape must not remove/alter the parent\n*226|  endpoint's ready/hosted state, and hosting must not churn-respawn.\n*227|  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,\n 228|  failing psyche → parent stays deliverable, no rehost churn, error stamped.\n 229|- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the\n 230|  failure budget counts real attempts (ledger-derived); a 12/min failure loop\n...\n 241|  close. Per-wave traceable activation (never full end-state upfront). Gates:\n*242|  isolated worktree per sha + fresh CARGO_TARGET_DIR, nextest, clippy\n 243|  --workspace, docs-drift xtask. `[twohost]` NO — but W4's int needs the\n 244|  **single-node multi-subnet rig** (the hall-bf shape: 2 subnets, one home).\n 245|- **Adapter (perri):** `psyche-ephemeral-parked` branch rebased once core API\n...\n 259|3. `{subnet}` fill naming — RESOLVED (doyle W4 Q1): a single `{subnet}` key,\n*260|   no `{home}` key (the endpoint home subnet is one concept; `home:` is already\n 261|   the subnet-name qualifier position). Confirmed against CONTEXT.md glossary.\n## F-035-CONFLICT.md\n 16|\n*17|- **Setup:** spools `spool_message_deferred_at(perch, \"peer\", …)` — which is\n 18|  `insert_message(…, WINDOW_ACTIVE_ONLY, …)` (spool.rs:363). So the row is\n 19|  **`active_only`**.\n 20|- **Trigger:** `spt api state idle <id>` — the real ACTIVE→IDLE edge.\n...\n 34|\n*35|For an **spt-hosted** endpoint (broker owns the PTY, `controllable`, relay-less), the\n 36|idle-edge/parked drain is the **ONLY** carrier for an `active_only` row. Traced every path:\n 37|\n 38|| carrier | `active_only` to spt-hosted? |\n...\n\n[Showing lines 1-467 of 858 (49.9KB limit). Use :468 to continue. Some lines truncated to 512 chars]"}],"details":{"scopePath":"src, tests, docs, CONTEXT.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":207,"fileCount":20,"files":["docs/BROKER-BRAIN-SPLIT-RESTORATION.md","docs/CONTEXT-MEMORY.md","docs/DEBUG-CONVERGE-PLAN.md","docs/DEBUG-ROLLOUT.md","docs/DEFERRED.md","docs/DOCS-STRATEGY.md","docs/DORMANCY-BUDGET.md","docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md","docs/F-035-CONFLICT.md","docs/F-035-RCA.md","docs/FAULT-MATRIX.md","docs/FLAKE-LEDGER.md","docs/GATEWAY-LIVENESS-DISPATCH.md","docs/GATEWAY-RCA-STEP1.md","docs/INJECT-SETTLE-REARM-GATE-VERDICT.md","docs/KNOWN-HAZARDS.md","docs/MANIFEST.md","docs/MSG-IDENTITY-DISPATCH.md","docs/MUSL-RELEASE-HANDOFF.md"],"fileMatches":[{"path":"docs/BROKER-BRAIN-SPLIT-RESTORATION.md","count":19},{"path":"docs/CONTEXT-MEMORY.md","count":5},{"path":"docs/DEBUG-CONVERGE-PLAN.md","count":7},{"path":"docs/DEBUG-ROLLOUT.md","count":7},{"path":"docs/DEFERRED.md","count":14},{"path":"docs/DOCS-STRATEGY.md","count":2},{"path":"docs/DORMANCY-BUDGET.md","count":7},{"path":"docs/F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md","count":20},{"path":"docs/F-030-PSYCHE-EPHEMERAL-DESIGN.md","count":19},{"path":"docs/F-035-CONFLICT.md","count":7},{"path":"docs/F-035-RCA.md","count":7},{"path":"docs/FAULT-MATRIX.md","count":11},{"path":"docs/FLAKE-LEDGER.md","count":3},{"path":"docs/GATEWAY-LIVENESS-DISPATCH.md","count":13},{"path":"docs/GATEWAY-RCA-STEP1.md","count":16},{"path":"docs/INJECT-SETTLE-REARM-GATE-VERDICT.md","count":1},{"path":"docs/KNOWN-HAZARDS.md","count":20},{"path":"docs/MANIFEST.md","count":20},{"path":"docs/MSG-IDENTITY-DISPATCH.md","count":8},{"path":"docs/MUSL-RELEASE-HANDOFF.md","count":1}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n  34│\n *35│1. The hot-swap that *is* wired into `apply` is a **per-session brain handoff** (`apply_brain_only` → `Brain::handoff`), whose job is endpoint survival (REQ-UPD-3), not running-process replacement.\n *36│2. By explicit design, `applyhost.rs:36-38` states: *\"The daemon's own resident loops keep running the old logic until its next restart (`ensure_running` spawns the executable path — now the new binary); that restart is routine lifecycle, never an endpoint cycle.\"*\n  37│3. So new code only goes live on a *subsequent, unrelated* daemon restart (next logon, or a manual restart — which is what made the *other* node, `hfenduleam`, show a new pid). The practical consequence on `enlyzeam`: `gerald`'s live daemon kept running 0.3.0 — including the message-envelope codec — so the box from the original bug report **was still reproducing the original `\\r` corruption even though the fix sat on disk.**\n  38│\n  39│The operator then asked the real question this document answers: **what would it take to make an update actually hand off to the new executable seamlessly — no dropped processes, internal loop timings consistent across the swap, plus any other angles that matter?**\n    │...\n  50│\n *51│- **broker** (stable kernel) — holds *only* the un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets, and (per the §B ownership table) the Iroh/QUIC endpoint + conn table. Minimal, versioned local IPC. Almost never updates.\n  52│- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk and re-attaches to broker-held handles.\n  53│\n  54│Explicit evidence the split was meant to be a **process** boundary:\n *55│- ADR-0004 Consequences: *\"A small internal broker **process** exists beneath the daemon — a deliberate, bounded walk-back of 'literally one process,' … guaranteeing endpoint survival across updates.\"*\n  56│- ADR-0004 rejected alternatives include **\"whole-daemon live FD-passing\"** and **\"drain + restart\"** — i.e. the chosen design is specifically *restart the brain, the broker process survives.*\n  57│- `M3a-PLAN.md`, `M3-PLAN.md`, `M3b-PLAN.md` all name \"the broker **process** + IPC (M3b).\"\n  58│- Spikes 01/03/04/05/06 each **PROVEN** with *two separate binaries* (`spt-spikes/spike-01-broker-handoff`): a PTY child + a live QUIC transfer survive 100× brain restarts, gapless and exactly-once. A spike that proves \"survives a brain restart\" presupposes the broker is a separate process.\n    │...\n  72│\n *73│- The brain cannot restart onto a new binary without killing the in-process broker thread — which would close every PTY, orphan every harness child, and drop every listening socket. So **the no-endpoint-drop self-update pillar is silently unrealized.**\n  74│- `apply` therefore performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — achieving nothing for a binary swap. `update.rs:234`'s statement that \"the live daemon execs the verified new binary\" is **aspirational; it was never wired** because the process split it depends on does not exist in production.\n  75│- Updates do not run until an unrelated restart/logon (observed live, Section 1).\n  76│\n    │...\n  78│\n *79│> **Glossary note already applied:** `CONTEXT.md` was sharpened during the session to state explicitly that there is **one broker per machine** (per `SPT_HOME`), *not* one per endpoint — a single broker hosts every endpoint's resources and is present even with zero endpoints online. This corrected a latent \"one broker per endpoint\" misconception and is load-bearing for decision Q2.\n  80│\n  81│---\n  82│\n    │...\n  86│\n *87│1. no endpoint or harness child process is dropped or suspended (REQ-UPD-3),\n  88│2. internal loop timings stay consistent across the swap,\n  89│3. a failed update fails *safe*,\n  90│4. the mechanism is uniform across Windows and Linux.\n    │...\n 101│\n*102│**Decision.** The broker process is the long-lived per-machine anchor. It owns the **seed-control lock + liveness** (the single-daemon-per-home invariant and the `ensure_running`/`is_running` ping target) and **supervises/spawns the brain** as its child. On update, the broker re-spawns the brain.\n 103│\n*104│**Reason.** The broker is the one component guaranteed present whenever the machine's daemon is alive — it runs even with zero endpoints (the \"bare daemon\" case, `daemon.rs:196`). The seed lock and liveness *must* survive a brain restart, or a second daemon could win the bind during the restart window and `is_running` would flap. Anchoring them in the never-restarting layer makes the single-instance invariant and liveness survive brain restarts for free.\n 105│\n 106│**Alternatives rejected.**\n 107│- *A thin third supervisor process spawning both broker and brain* — adds a process for no invariant the broker can't already hold.\n*108│- *Brain as parent, spawning the broker* — incoherent: a brain restart would orphan or kill its own broker child, dropping endpoints.\n 109│\n*110│**Operator's challenge that strengthened this.** The operator initially held a \"one broker per endpoint\" model and correctly objected that liveness shouldn't live in something that only exists when an endpoint is online. The code shows one broker *per machine*, always present, so the objection's principle is satisfied and actually reinforces putting liveness in the broker.\n 111│\n 112│### Q3 — Update trigger: brain self-exit, broker auto-respawn from disk\n 113│\n    │...\n 125│- **Periodic resident loops** (pump cadences, pulse/echo-commune): persist a `(anchor, interval)` pair **once** on a fresh/crash start. The next fire is derived functionally — `next_fire = anchor + interval × ⌈max(0, now − anchor) / interval⌉` — with **no per-fire writes**. An **update restart re-reads `(anchor, interval)` and keeps deriving** (phase preserved, lands mid-grid). A **crash restart is treated as a fresh start** (rewrite `anchor = now`; phase reset is acceptable).\n*126│- **One-shot scheduled events** (alarms): persist the absolute `target-time` **at creation**; every start (update *or* crash) reads it and fires-if-due. **These never reset on crash** — a user's \"remind me at 3pm\" is a commitment that must outlive any restart. **[V3]** The one-shot *rule* is specified this milestone, but the *machinery* is **built with the alarm port, not here**: the daemon has no one-shot consumer today (§7), so building it now would ship untested dead code (matches the project's act...\n 127│\n 128│The brain distinguishes update-restart from crash/fresh-restart via a signal on the broker's respawn (the broker *initiated* the planned cycle but *observed* the unexpected crash). The `(anchor, interval)` write happens once per fresh/crash start, zero per update, zero per fire.\n 129│\n    │...\n 169│\n*170│**Decision.** If the new brain fails to reach a **ready** signal (re-attached all sessions + resumed loops) within a bounded number of boots / a healthy-run window, the broker **rolls back to the last-known-good binary** (the `spt.exe.old-N` backup), **quarantines** the bad version (no auto re-apply/re-fetch), and surfaces a loud consent-style notification. The broker holds **last-known-good + candidate** paths and chooses which to spawn (no file manipulation at failure time). Endpoints survive throughou...\n 171│\n 172│**[V1] Rollback-state-compat invariant (forward invariant, mint this milestone).** Auto-rollback spawns the **old** binary against durable state the **new** brain may already have written. This is safe *today* (a source audit confirmed zero state-migration code exists), but the first release that migrates a durable-state schema would silently break Q7's rollback. The invariant must be minted now, while it is free: **a brain must not irreversibly migrate durable state before ready-promotion** — equivale...\n 173│\n 174│**Reason.**\n*175│- The dangerous case is a *runtime* bug (new brain panics on boot), not the binary land (already rolled back at `applyhost.rs:167-170`) and not IPC mismatch (the `classify` handshake `brain_ipc_version ≥ broker.min_compatible` is checked pre-swap). A naive supervisor would crashloop the new binary: endpoints alive but logic dead — \"up but useless.\"\n 176│- The backup binary is already produced for exactly this, and the readiness+backoff machinery already exists — `peerloop.rs:82-91` has `SUPERVISE_BACKOFF_BASE/CAP` + `SUPERVISE_HEALTHY_RUN` (60 s healthy resets backoff), and `supervise_pump` is wired at `peerloop.rs:805` (M8-D4, verified). A bad update that bricks *logic* should self-heal back to last-good without a human, the same spirit as the boot-race self-heal already shipped.\n 177│- The two-phase record fixes a real existing bug: `applyhost.rs:176` writes `record_applied(version)` + `last-outcome=applied` **before** the brain handoff — i.e. before we know the new brain boots. (This is the optimistic `applied.json={version:6}` observed on `enlyzeam`.)\n 178│\n    │...\n 192│- A new hidden `spt daemon brain` entry is the **brain** process: connects to the broker, runs pump/dispatch/psyche loops/session drivers, rehydrates from disk, emits a `ready` signal.\n*193│- `ensure_running` / `is_running` / `daemon stop` contracts are unchanged — they target the broker's seed channel.\n 194│- The de-elevation guard (`daemon.rs:60-103`) applies at the **broker** entry; the brain child inherits the unelevated identity.\n 195│\n 196│---\n    │...\n 199│\n*200│This is **milestone-sized**, not a patch. It includes: the process decomposition; an IPC-verb surface for the hard cases (digest push, seed-lock/liveness, plus the general direct-call audit — net bring-up is near-free per the §Q5 audit correction); broker-side per-session delivered-cursor tracking; broker-owned generation custody handed at spawn **[V2]**; durable absolute-deadline timing across **phase-significant** resident loops only **[V4]** (the idempotent pump cadences need *no* conversion — co...\n 201│\n 202│### 6.1 Sequencing — RATIFIED (operator, 2026-06-09)\n 203│\n    │...\n 215│\n*216│- **Alarms have no durable daemon scheduler.** In the spt-core daemon, `alarm` exists only as an event *shape* (`spt-proto`) + relay handling (`psyrelay.rs:93`) + a test fixture; `target-time` is only a wire attribute. The actual \"wait until target, fire the TIMED PULSE\" timer lives in the legacy owl listener in-memory. Porting alarms into the daemon as a durable scheduler (which would then ride the Q4 one-shot mechanism) is its own gap.\n 217│- **Registry one-cadence reconverge window.** `RegistryHost` is intentionally in-memory (`registryhost.rs:19-26`): a restart forgets peers' rows and reconverges within one pump cadence (self-rows re-advertised each tick; mirrored to `identity/registry/` JSON for out-of-process readers). Across a brain restart this is a brief reconverge window, accepted by design — not a blocker, noted for completeness.\n 218│- **Multipart inter-agent message truncation — belongs to LEGACY `claude_skill_owl`, not spt-core.** During the verification exchange (which ran over the legacy live-agent messaging infra — the `owl.exe` listener, not the spt-core daemon), an 18-part chunked message reached the recipient as ~parts 1–8; a long resend also dropped its tail. Long messages silently lose their tail — a delivery-integrity issue in the chunk-reassembly path. **This is a legacy-project bug** (the agents in this exchange ...\n 219│- **Stale DEFERRED mesh row.** Side-flag from verification: the mesh row in `docs/DEFERRED.md` is stale (mesh shipped at v0.3.0). Minor cleanup, noted for a docs pass.\n    │...\n 225│- **ADR-0004** — `docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md` (the decision being restored; §A update classes, §B ownership table, the spike amendments).\n*226│- **Requirements** — REQ-UPD-3 (no-endpoint-drop, brain-only), REQ-UPD-4 (staged + consent), REQ-UPD-6/8 (update-set / platform-safe), REQ-DAEMON-3/9 (auto-start / net self-heal), REQ-HAZARD-UPDATE-ROLLBACK, REQ-HAZARD-HANDOFF-ARGV-COMPAT.\n 227│- **Glossary/model** — `CONTEXT.md` daemon / broker / brain (incl. the one-broker-per-machine clause added this session).\n 228│- **Spikes** — `docs/spikes/SPIKE-01-broker-handoff.md`, `-03-quic-survival`, `-04-forkpty-parity`, `-05-restart-stress`, `-06-idempotent-boundary` (proofs that the two-process handoff works on both OSes).\n 229│- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.\n    │...\n 238│- **`REQ-*` registry mints** (registry-first, `traceable-reqs.toml`; minted INACTIVE per rule 5, activated per `RESTORATION-PLAN.md`'s schedule):\n*239│  - **`REQ-HAZARD-BROKER-PROCESS-ISOLATION`** — a brain restart must never drop a hosted endpoint (REQ-UPD-3's real promise). **Evidence [V5]:** an `int` stage productionizing SPIKE-01/03 — a PTY child + a live QUIC conn survive a brain-*process* restart onto a *swapped* binary. REQ-UPD-3 / REQ-DAEMON-2's current `int` evidence (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`) proves only the *in-process* handoff shape — **mis-evidenced today, re-pointed at plan task D7.**\n 240│  - **`REQ-HAZARD-ROLLBACK-STATE-COMPAT` [V1]** — a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable).\n 241│  - The functional two-process-split realization + the **[V6]** N-1 old-broker × new-brain CI compat test are carried as `RESTORATION-PLAN.md` tasks D1–D7 (the compat test lands as a D7 gate).\n 242│- **KNOWN-HAZARDS** §6.7 (in-process collapse) + §6.8 (rollback-state-compat), with condensed-checklist rows.\n## CONTEXT-MEMORY.md\n  6│\n *7│- **live context** (per-agent, project-independent — the agent's *mind*): identity/persona, relationship + rapport with the user, the user's cross-project preferences & working style, cross-project life threads, **learnings from work that may apply to other projects**, a thin **project roster** (one line per project the agent works on), the agent's self-model. Synced to **all** instances of the endpoint.\n  8│- **project context** (per-agent-per-project — what the agent is doing *here*): codebase/architecture, decisions made here, **current focus / next steps / todos**, project conventions, build/test commands, blockers. Synced **only** to same-project instances.\n  9│\n 10│(This split independently matches pi-agent-memory's Project Store vs Global Store and its discriminator dialogue — convergent validation.)\n   │...\n 30│- **`/compact`** → resume Self → **commune** file-drop.\n*31│- **`spt suspend`** → resume Self → **signoff** file-drop (suspend = the endpoint going down, so it's a signoff, not a commune).\n 32│\n 33│This deliberately swaps the project-centric echo for a Self-authored capture at exactly the moments that matter (context loss + suspend), so live context stays current. (Routine cadence communes remain echo — project-primary, live-conservative.)\n 34│\n   │...\n 36│\n*37│A lever for an agent behind an **spt-hosted** endpoint to clear and resume itself without stalling. It: (1) triggers a `/clear` (which uses the **Self-resume-commune rule** above — consistent with all other boundaries), (2) thereby captures a commune, and (3) **guarantees a resume signal hits the harness post-clear** so the agent's turn restarts and it resumes work — starting from its immediate next-steps. Spt-hosted-only because the guarantee requires the daemon to own the PTY and inject the post-cl...\n 38│\n 39│### Ownership\n 40│\n   │...\n 73│\n*74│A generated, lightweight **INDEX** over topics — summaries + concepts + aliases + paths — **not** a vector DB, **not** a full mirror (adapted from pi-agent-memory's Retrieval Map). One INDEX per **project** (aggregating all agents' project context → realizes cross-agent synthesis + query-routing: \"which endpoint owns this task\") and per **agent**. Regenerated on context change; the merge driver + INDEX regeneration keep it current.\n 75│\n 76│## Start-injection: dense working set, sparse tail\n 77│\n   │...\n 82│- **Hot/cold curated by the Psyche** via `Pinned` + recency; keeping the hot set tight is part of maintaining memformat.\n*83│- **Tunable** via a working-set **density knob** (spt-core global setting + per-endpoint override).\n 84│\n 85│## Deferred\n 86│\n## DEBUG-CONVERGE-PLAN.md\n  19│After a coordinator stages a debug rollout, answer one question without\n *20│hand-walking nodes: **did every expected lab node converge on the target debug\n  21│version, and if not, why not?**\n  22│\n  23│`debug-converge` watches until every *expected* debug-pinned reachable node\n *24│reports the target version applied, or it returns a per-node timeout table.\n  25│This is what turns \"fast to stage\" into \"fast to *verify deployed*\" — the\n  26│actual deployment-speed win the rollout pipeline exists for.\n  27│\n    │...\n  43│|---|---|---|\n *44│| `--version <u64>` | required | target debug version to converge on |\n  45│| `--nodes <id,…>` | derived | explicit expected-node set; overrides derivation |\n  46│| `--subnet <name>` | the rollout's lab subnet | scope derivation to one subnet |\n  47│| `--timeout <secs>` | 120 | overall watch budget |\n    │...\n  55│still *expected* and shows up as a row, not a silent omission. `--nodes` gives\n *56│an explicit set for targeted tests.\n  57│\n  58│## Per-node state model\n  59│\n    │...\n  65│|---|---|---|\n *66│| `Applied` | node reports `current debug version == target` | ✓ success |\n  67│| `NotPinned` | node is on the `stable` channel — rejected at the metadata gate | ✓ (expected-set bug or intentional) |\n  68│| `Offline` | unreachable within the round | pending → timeout |\n  69│| `StagedAwaitingConsent` | candidate staged, default-gated, no apply ack yet | pending |\n *70│| `BlockedByBrokerResources` | broker-touching candidate refused while live endpoints held | pending (operator must quiesce) |\n  71│| `Rejected{reason}` | typed refusal: `NoArtifactForPlatform`, `WrongChannel`, `Rollback`, `ArtifactMismatch`, … | ✓ failure |\n  72│\n  73│Convergence = every expected node is `Applied`. Any node still pending when the\n    │...\n 102│```\n*103│DEBUG_CONVERGE target=7 channel=debug subnet=lab expected=3\n 104│  node-a   Applied                v7\n 105│  node-b   StagedAwaitingConsent  v6  (run: spt update apply)\n 106│  node-c   Rejected               NoArtifactForPlatform (x86_64-apple-darwin)\n## DEBUG-ROLLOUT.md\n  15│- A rollout is a signed `SignedUpdateSet`: one signed metadata record, with one\n *16│  artifact digest per Rust target triple.\n  17│- Each recipient verifies the set under its node-local `release-keys.json`,\n  18│  selects only its own platform artifact, stages it, and then follows the normal\n  19│  consent/apply policy.\n    │...\n  25│- A broker-touching candidate may be staged, but apply refuses it while\n *26│  broker-held runtime is live. Shut down or suspend hosted endpoints first; the\n  27│  first flow does not auto-cycle them.\n  28│\n  29│## One-time lab setup\n    │...\n  63│- `--build-current` builds and includes the coordinator's current platform.\n *64│- `--artifact <target=path>` adds an already-built artifact for another target,\n  65│  such as `x86_64-pc-windows-msvc` or `x86_64-unknown-linux-gnu`.\n  66│- `--version <u64>` overrides the local debug sequence. Use a higher value when\n  67│  recovering from lost local state.\n    │...\n  69│- `--state <path>` stores the local debug sequence somewhere other than\n *70│  `target/debug-rollout-state.json`.\n  71│\n  72│The staged update then propagates through the normal pull-based update pump.\n  73│There is intentionally no bespoke push path.\n    │...\n  96│`Applied` / `StagedAwaitingConsent` / `NotPinned` / `Rejected{reason}` /\n *97│`Offline` — exiting `0` only when every node applied the target (`1`\n  98│otherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\n  99│The manual walk below remains the fallback.\n 100│\n    │...\n 103│- `NoArtifactForPlatform` means the signed set did not include the recipient's\n*104│  Rust target triple.\n 105│- `WrongChannel` means the node is not pinned to `debug`.\n 106│- `Rollback` means the offered debug version is not greater than the node's\n 107│  current debug-channel version.\n 108│- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held\n*109│  resources; quiesce hosted endpoints first.\n 110│\n 111│## Agent checklist\n 112│\n## DEFERRED.md\n 10│| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |\n*11│| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one...\n 12│| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |\n 13│| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant s...\n 14│| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |\n 15│| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |\n*16│| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |\n 17│| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |\n*18│| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |\n 19│| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the ...\n 20│| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concr...\n 21│| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect,...\n   │...\n 26│| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n*27│| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite ho...\n*28│| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\n 29│| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache...\n 30│| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n*31│| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`w...\n*32│| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version ...\n 33│| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n*34│| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer \"serving subnet X?\", a...\n 35│| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures...\n*36│| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual \"Apply and observe\" (`DE...\n 37│| **Origin-source update bootstrap (`spt update fetch` — pull the latest signed release from GitHub into the existing verify→stage→apply pipeline)**: today update DISCOVERY is **peer-only** (REQ-UPD-1: a daemon pulls offers from roster peers via `request_update`; `propagate.rs`/`peerloop.rs`). There is NO origin fetch, so the **first node in a fleet — or any isolated/solo node — cannot get a new release without a peer that already staged it** (the fleet converges by gossip, but only *after* a m...\n 38│| **Test broker socket-bind hardening (CI-flake removal)**: `applyhost` tests' `served_broker` helper does `Broker::bind(name).expect(\"bind broker\")` (`crates/spt-daemon/src/applyhost.rs:293`) where `name` = `spt-daemon-d7b-{pid}-{seq}.sock`. Flakes intermittently on the **kitsubito** self-hosted Linux runner (likely AF_UNIX path collision / stale socket file under the runner's reused workspace — Windows named pipes don't hit it). Cost the v0.3.2 release a full red CI run + a `--failed` rerun (2026-06-...\n 39│| **`spt update fetch` single→set shape-upgrade at same version (REQ-UPD-8 transitional trap)**: `cmd_update_fetch` sets the rollback floor = `staged_version` (the monotonic counter), and the gate is a plain `candidate > floor`. A node that fetched+staged version N as a platform **single** (pre-0.3.2 fetch behavior) can never re-fetch version N as a multi-platform **set** — same counter → `Rollback{current:N, candidate:N}`. Bites the **seed-node bootstrap**: the first node to origin-`fetch` a relea...\n   │...\n 42│| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers m...\n*43│| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's \"remind me at 3pm\" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today...\n*44│| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **aut...\n 45│| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n*46│| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47│| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n 48│| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n*49│| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n*50│| **Always-on endpoint kind + `#`-sigil addressing (ADR-0023; REQ-EP-8 / REQ-EP-9) — the core seam for adapter-bridged group/forum messaging**: a `[always-on]` manifest section declaring a resident, owner-less, addressable endpoint binary the daemon boot-launches + supervises **continuously** (independent of agent liveness, register-triggered, one per `<adapter>[:profile]`), which self-manages its `#`-addressed channel endpoints via the existing `api bind` and may call the already-ungated `endpoint wak...\n## DOCS-STRATEGY.md\n 27│\n*28│## Killer quickstart targets (two, one per audience)\n 29│\n 30│- **Human dev:** two agents exchange a message end-to-end in **one command + minimal config**, < 10 min — the core primitive, whole-thing-first, then decomposed.\n 31│- **Dev-agent integrator:** a minimal **adapter** that satisfies the manifest + `api` contract and drives one lifecycle operation — the \"build a harness for spt-core\" hello-world.\n   │...\n 83│- [Quickstart: build an adapter](…/quickstart/adapter.md): minimal manifest + `api` contract.\n*84│- [Mental model](…/concepts/overview.md): daemon, endpoints, instances, subnet.\n 85│\n 86│## Capabilities      (one bullet per vertical, each → its own llms.txt)\n 87│- [Messaging](…/messaging/llms.txt): typed ops + file blobs; send / ring / ready.\n## DORMANCY-BUDGET.md\n  25│   per-seat overhead: PTY plumbing + output rings);\n *26│5. kills every seat (the **suspended** state — only the on-disk perch record\n  27│   would remain in production) and samples the residual.\n  28│\n  29│Sampling: Windows via `Get-Process` (WorkingSet64 / HandleCount /\n    │...\n  63│residual: host process returns to its ~6–7 MiB baseline — a suspended seat\n *64│costs **only the on-disk perch record** (bytes, not megabytes, no handles, no\n  65│CPU).\n  66│\n  67│### gravity (Ubuntu 22.04, 2026-06-04, `[budget]`-tagged CI run @ 785002e)\n    │...\n  91│   amended. The knob chain shipped in D9-2 (`auto_suspend_after_ms`: global\n *92│   default OFF → `daemon.json` node leg → `info.json` endpoint leg, `0` =\n *93│   explicit endpoint OFF) is the policy surface.\n  94│3. **Constrained nodes should default the node leg ON.** ~300 MiB/seat is\n  95│   real on a Pi/handheld-class node: set `auto_suspend_after_ms` in\n  96│   `daemon.json` there. The threshold counts from **dormancy onset** (the\n  97│   `dormant_since_ms` anchor), never from last activity.\n *98│4. **Heavy-harness endpoints may opt themselves out/in per seat** via the\n *99│   endpoint override — e.g. a billable harness whose warm session holds a\n*100│   cost/footprint beyond RSS (seat-count limits) sets a per-endpoint\n 101│   threshold even on a big node. Note: an **idle** claude seat holds no API\n 102│   spend (tokens flow only on input); the footprint concern there is seat\n 103│   count and memory, not per-minute billing.\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n  *1│# F-027 — ENDPOINT_SPAWN_FAIL: pre-spawn seam + bind-deadline honesty (DESIGN)\n   2│\n   3│Status: **FROZEN 2026-07-03 — field review complete.** Author: doyle. Inputs:\n   4│perri field seeds + spawn-fail taxonomy (2026-07-03, 16-part dispatch,\n    │...\n  18│\n *19│`spt endpoint run <id> --start` prints `ENDPOINT_RUN_STARTED` when the broker\n  20│has accepted the spawn RPC and a child PID exists — **nothing more is verified**\n  21│(cli.rs:1819 machine line, cli.rs:1833 `--start` success print). A harness that\n  22│launches but wedges post-spawn (workspace-trust prompt, null stdin) never fires\n *23│its SessionStart hook, so `establish_perch` never runs (api/startup.rs:299 is\n *24│the ONLY writer of the `ready` marker), and the endpoint sits `ready=false`\n  25│**forever, silently** — while the operator was told it started.\n  26│\n  27│`ready=false` is already rendered by `spt list`/picker (roster.rs:69 →\n  28│cli.rs:2735/3826) but nothing gates or alarms on it: passive display only. The\n *29│attach path's `await_endpoint_session` (cli.rs:1854, 25 s) gates on broker\n  30│**session existence** — created synchronously at spawn, so it passes instantly\n  31│for a wedged harness — and the `--start` path skips even that.\n  32│\n    │...\n  35│KNOWN-HAZARDS F-019 lesson). Any F-027 token must land on **CLI stderr and/or\n *36│the perch record**, never daemon-stderr-only.\n  37│\n  38│## 2. Field evidence (perri, 2026-07-03 — provenance table)\n  39│\n    │...\n  41│|---|------|----------|-------|\n *42│| 1 | TRUST-WEDGE — CC at workspace-trust dialog, null stdin; `ENDPOINT_RUN_STARTED` printed; ready=false forever | every fresh probe cwd, this session | SILENT |\n  43│| 2 | ACCOUNT-ROOT-MISMATCH — trust seeded in a DIFFERENT config root than the spawn resolves; wedges despite a valid-looking seed | Reavo/Brandon dual-root box (si1 vs b6b probes) | SILENT |\n  44│| 3 | PSYCHE-CRASHLOOP — nested psyche CC dies at trust dialog; wrapper respawned ~3/s, ordinal 5358 in 30 min | F-h; now guarded by adapter v0.13.0 give-up + core C3(b) | SILENT (guarded) |\n  45│| 4 | ADAPTER-UNRESOLVED — no registered adapter hosts the parent binary | seed→listen path, bash parent | LOUD pre-spawn (good shape) |\n  46│| 5 | STALE-CLI-CONTRACT — bringup recipe passes a removed flag; \"unexpected argument\"; spawn never happens | old recipe on 0.22.0+ | LOUD pre-spawn (good shape) |\n *47│| 6 | RELAUNCH-NEVER-BINDS — 2nd `endpoint run` same id after tree-kill; prints but never binds | **0.22.0 only — FIXED on 0.23.0 by F-028 B1/B4** (perri definitive repro 2026-07-03: tree-kill → same-id re-run re-binds fresh session, live PTY, honest info.json; first check was rig-void, --subnet omitted, caught before reporting) | FIXED-NOTE (not in F-027 scope) |\n  48│| — | UAC-740 elevation spawn-fail | NOT observed (subnet-op elevation only) | out of scope |\n  49│\n  50│Seed-1 correction (accepted): the \"backslash-keyed `hasTrustDialogAccepted`\n    │...\n  65│\n *66│Target invariant (headline): **\"started\" means BOUND, or the failure is LOUD.**\n  67│The silent class (modes 1, 2, 3, 6) must fail with the same loudness as the\n  68│pre-spawn class (modes 4, 5) already does.\n  69│\n    │...\n  71│\n *72│After the spawn RPC returns in `cmd_endpoint_run --start`, the CLI **awaits\n  73│bind** — the appearance of the `ready` marker (`resolve_ready_file`, the\n *74│`establish_perch` effect) — bounded by a deadline:\n  75│\n *76│- **Success:** `ENDPOINT_RUN_STARTED:{id}` moves to AFTER the bind is observed\n *77│  — its plain meaning finally true. The early machine line `ENDPOINT_RUN:{id}\n  78│  … pid={pid}` stays at spawn-accept (scripts parse it; unchanged).\n *79│- **Deadline exceeded:** emit **`ENDPOINT_SPAWN_FAIL:{id}: reason=<CLASS>:\n *80│  <detail>`** on CLI stderr, exit non-zero, and **stamp the perch record**\n  81│  (`spawn_fail` field via the locked `mutate_info` RMW) so `spt list` and the\n  82│  picker render the honest state. A later successful bind clears the stamp\n *83│  (`establish_perch` already rewrites state; clearing rides there).\n  84│- Classification probes (all CLI-observable; no daemon change needed to\n  85│  detect): child pid alive? broker `has_session` still true? ready marker\n  86│  present? →\n    │...\n  97│  hatch `--no-wait` restores fire-and-forget and prints\n *98│  `ENDPOINT_RUN_DETACHED:{id}` (explicitly unverified — never `_STARTED`).\n *99│- The attach (non-`--start`) path upgrades the same way: `await_endpoint_session`\n 100│  keeps its session gate, then the same bind-await runs before declaring health.\n 101│\n 102│Failure of the await machinery itself (CLI killed mid-wait) fails open to\n    │...\n 116│   env vars the adapter declares `[env.<NAME>] direction=\"read\"` are **captured\n*117│   at endpoint creation and stamped into the perch record**; the broker threads\n 118│   the IDENTICAL stamped values to BOTH the pre-spawn command AND the session\n 119│   spawn. Neither consumer reads its own process env for a stamped var — if the\n 120│   seeder read its own env while the spawn read the stamp (or vice versa),\n    │...\n 132│3. **Bounded + loud:** runtime capped (30 s); non-zero exit or timeout ABORTS\n*133│   the session spawn with `ENDPOINT_SPAWN_FAIL:{id}: reason=PRE_SPAWN_FAIL:\n 134│   <exit/detail>` on CLI stderr (relayed through the spawn RPC error — this\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n  17│\n *18│The daemon hosts each live endpoint's Psyche as a **long-lived resident\n  19│process** and enforces that residency:\n  20│\n  21│- `livehost.rs:497 → host_one:518` spawns the psyche\n    │...\n  48│exit after the seed turn tripped the residency machinery, which tore down the\n *49│endpoint's hosted state — ready marker removed, never re-stamped, every\n  50│force-native gated `leg=cli-gate-not-hosted` **permanently** (field brick,\n  51│operator-hit on hall-bf, contained same-day by v0.13.3).\n  52│\n    │...\n  77│\n *78│Target invariant (headline): **the Psyche is a per-event turn, not a process.\n  79│Liveness of the psyche = \"turns succeed\", never \"a pid is resident.\"**\n  80│\n  81│### W1 — wire the per-event driver (retire the resident spawn)\n    │...\n  95│The daemon drives every invocation, so every substitution key is filled from\n *96│**daemon-known context** — the child never self-resolves home/subnet/perch.\n  97│Direction-(a) failures become impossible by construction, not by fixing the\n  98│poll.\n  99│\n    │...\n 108│The psyche mints and keeps **its own session id**, stored in the nested\n*109│`{id}-psyche` perch record — `{session_id}` in psyche role templates becomes\n 110│**the psyche's sid**, never the parent's (today's fill at livehost.rs:518 is\n 111│the parent's — the custody bug). Rules:\n 112│\n    │...\n 131│  resident child. REQ-HAZARD-LIVEHOST-NONRESIDENT's **spirit** (a psyche that\n*132│  cannot work must be visible on the perch, and hosting must not churn)\n 133│  transfers to the W1 failure budget; the REQ's traceable entry gets a\n 134│  SUPERSEDED pointer to the new REQ (same pattern as LIVENESS-DECAY →\n 135│  SUPERSEDED in C-1).\n 136│- **The teardown that made direction (b) destructive is deleted**: no psyche\n*137│  state may ever remove the PARENT endpoint's ready marker or hosted state.\n 138│  Psyche trouble stamps psyche fields only. (This is the conformance test's\n 139│  core assertion.)\n 140│- C3(b) guard rework rides here (evidence #6): the failure budget counts\n*141│  **attempts via the psyche perch ledger** (boot records / turn records), not\n 142│  whatever it counted that let 12/min churn run invisibly. Red-first: a\n 143│  12/min synthetic failure loop MUST trip the budget.\n 144│\n    │...\n 146│\n*147│Even fully ephemeral, child-side verbs acting on nested perches must resolve\n 148│without a `--subnet` the child cannot know. Fix in core, general form:\n 149│**nested `{parent}/{nested}` ids resolve under the parent's home** —\n*150│subnet-less resolution for nested perches (`resolve_perch_path`/ready-verb\n 151│family). Additionally expose a single `{subnet}` base_keys fill **when\n 152│known** (lifecycle.rs:338; absent → LOUD missing-key fail, the `{node}`\n 153│precedent on :344). **There is deliberately NO `{home}` key** (doyle W4 Q1: the\n*154│endpoint home subnet is one concept, and `home:` is already the subnet-name\n 155│qualifier position — a `{home}` template key would only invite meaning-drift).\n 156│Red-first = evidence #3's exact repro: 2-subnet home, nested-id verb, must\n 157│succeed.\n    │...\n 188│  boundary holds).\n*189│- No relay-unregister fix (the post-clear `owl send → NO_PERCH` casualty) —\n 190│  separate seed, C-2 follow-up (a), field-repro'd 2026-07-04, sequenced with\n 191│  the operator's v0.24.0 bug batch, likely same milestone but its own item.\n 192│- No wake-arc work — closed end-to-end 2026-07-04 (five layers, wakefin3 +\n    │...\n 212│  bounded per-event turn through resume/turn/relay; no resident psyche process\n*213│  exists between events. Red-first: fire an event on a hosted live endpoint →\n 214│  assert one turn ran (side-effect proof file — transcript-based asserts are\n 215│  structurally blind, 2026-07-04 rig lesson) and no `{id}-psyche` process\n 216│  survives the turn.\n    │...\n 218│  parent boundary does not rotate it; reseed is loud. Red-first: parent\n*219│  `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's\n 220│  — guard-revert reproduces).\n*221│- `REQ-PSYCHE-NESTED-RESOLUTION` — nested-perch verbs resolve under the parent\n 222│  home without `--subnet` on a multi-subnet node; a single `{subnet}` fill\n 223│  present-or-loud (no `{home}` key). Red-first: evidence #3 repro (2-subnet home rig).\n 224│- `REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION` — hazard (paid-for, hall-bf +\n 225│  0.13.2): psyche failure of ANY shape must not remove/alter the parent\n*226│  endpoint's ready/hosted state, and hosting must not churn-respawn.\n*227│  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,\n 228│  failing psyche → parent stays deliverable, no rehost churn, error stamped.\n 229│- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the\n 230│  failure budget counts real attempts (ledger-derived); a 12/min failure loop\n    │...\n 241│  close. Per-wave traceable activation (never full end-state upfront). Gates:\n*242│  isolated worktree per sha + fresh CARGO_TARGET_DIR, nextest, clippy\n 243│  --workspace, docs-drift xtask. `[twohost]` NO — but W4's int needs the\n 244│  **single-node multi-subnet rig** (the hall-bf shape: 2 subnets, one home).\n 245│- **Adapter (perri):** `psyche-ephemeral-parked` branch rebased once core API\n    │...\n 259│3. `{subnet}` fill naming — RESOLVED (doyle W4 Q1): a single `{subnet}` key,\n*260│   no `{home}` key (the endpoint home subnet is one concept; `home:` is already\n 261│   the subnet-name qualifier position). Confirmed against CONTEXT.md glossary.\n## F-035-CONFLICT.md\n 16│\n*17│- **Setup:** spools `spool_message_deferred_at(perch, \"peer\", …)` — which is\n 18│  `insert_message(…, WINDOW_ACTIVE_ONLY, …)` (spool.rs:363). So the row is\n 19│  **`active_only`**.\n 20│- **Trigger:** `spt api state idle <id>` — the real ACTIVE→IDLE edge.\n   │...\n 34│\n*35│For an **spt-hosted** endpoint (broker owns the PTY, `controllable`, relay-less), the\n 36│idle-edge/parked drain is the **ONLY** carrier for an `active_only` row. Traced every path:\n 37│\n 38│| carrier | `active_only` to spt-hosted? |\n   │...\n 41│| Send-time idle re-offer (`cli.rs:5734`) | **NO** — gated `&& !active_only`. |\n*42│| Hook poll `drain_active_window` (`api poll`) | **N/A** — that is the HARNESS-hosted hook channel; an spt-hosted endpoint has no `api poll` harness. |\n 43│| **Idle-edge / parked drain `drain_idle_spool`** | **YES** — the F-023 leg-2 behavior (`include_deferred = !resting`). **This is the only carrier.** |\n 44│\n 45│So: `active_only` is designed as *\"the active **hook** window\"* — meant for\n*46│**harness-hosted** endpoints that `api poll`. An **spt-hosted** endpoint has no hook, so\n 47│F-023 leg 2 rescued its `active_only` rows on the idle edge — otherwise they starve with\n 48│zero carriers. **My F-035 fix removes that only carrier → permanent starvation for\n 49│`active_only`→spt-hosted.**\n   │...\n 54│\n*55│What is correct for an `active_only` row destined to a relay-less **spt-hosted** endpoint,\n 56│which has no hook-poll carrier?\n 57│\n 58│- **(A) never-wake wins** — F-035 fix stands; F-023's starvation guarantee for\n   │...\n 63│  (F-023 correct). Then **F-035 is mis-scoped**: *\"never wakes\"* is the **harness** hook\n*64│  model (harness endpoints DO hook-poll, so they never need the idle edge for it); it was\n 65│  never a contract for the hook-less spt-hosted class. **RETIRE or NARROW\n 66│  REQ-SEND-WINDOW-DRAIN-HONOR; no code change.**\n 67│- **(C) class split** — spt-hosted delivers `active_only` at idle **without waking**\n   │...\n 71│**todlando lean:** the carrier table points at **(B)** — the field-doc *\"never wakes an\n*72│idle target\"* (cli.rs:85) describes the harness hook model, and F-023 already reasoned\n 73│through the spt-hosted no-hook gap deliberately. The original F-035 *symptom* (a\n 74│`--deferred` background row reaching an idle spt-hosted agent) may be **working as\n 75│designed** — it is the only way that class can ever be delivered. If the true concern is\n## F-035-RCA.md\n  16│- `spt send --active-only` (and the hidden back-compat alias `spt send --deferred`)\n *17│  — `cli.rs:85` documents the flag verbatim as **\"never wakes an idle target\"**.\n  18│- `send_deferred(...)` shell-context background rows (`reporting.rs:161`) —\n  19│  hook-channel background context that must ride only the active poll.\n  20│\n *21│For an **spt-hosted, relay-less** endpoint (the class the idle-edge drain targets),\n  22│such a row is delivered through the translation binary at the idle window — the\n  23│exact delivery the tag forbids.\n  24│\n    │...\n  69│```\n *70│let include_deferred = !crate::resting::deferred_held(&perch_path);\n  71│```\n  72│\n *73│So whenever the endpoint is **not resting**, `include_deferred = true` → the idle\n  74│claim is `WHERE delivered=0` → it takes **`active_only` (deferred=1)** rows and\n  75│injects them at the idle window.\n  76│\n    │...\n  94│- **\"`active_only` rows never reach this spool.\"** Refuted — `spt send --active-only`\n *95│  / `--deferred` and `send_deferred` both mint them into the same per-perch spool\n  96│  `claim_idle_edge_inner` reads. Live, not theoretical.\n  97│- **\"The resting gate already handles it.\"** Refuted — it only excludes them while\n *98│  *resting*; the active→idle edge on a non-resting endpoint is the leak path.\n  99│- **\"`idle_only` is symmetric-broken too.\"** No — `idle_only` has `deferred = 0`, so\n 100│  both idle-claim branches include it; it correctly rides the idle window. Only the\n 101│  `active_only`→idle direction is wrong.\n    │...\n 134│  of resting state (the resting-orthogonal assertion).\n*135│- **int**: an `active_only` row present at an active→idle edge (endpoint **not**\n 136│  resting, live translation binary) is **NOT** injected — it stays spooled for the\n 137│  active hook poll — while a `default` and an `idle_only` row on the same edge **are**\n 138│  injected. Red-first: pre-fix the `active_only` row injects.\n## FAULT-MATRIX.md\n 20│| 1 | **Brain killed mid-PTY-stream** (logic crash / routine self-update) | Logic halts until restart | Broker, PTY child (pid stable), output log | New brain re-subscribes from cursor; gapless + exactly-once | `spt-daemon/tests/` B2/B9 handoff suite; `tests/idempotent.rs` (crash before-intent / before-effect / after-effect) |\n*21│| 2 | **Brain killed mid-QUIC-stream, receive side** | Logic halts | Broker-owned endpoint, conn, stream, read ring | Resubscribe from durable cursor; ring replays the dead window | `tests/netstream.rs::receiver_brain_restart_is_gapless_and_exactly_once` |\n 22│| 3 | **Brain killed mid-QUIC-stream, send side** | Logic halts | Broker-owned conn/stream + effect journal | Re-drive whole durable op sequence; journal dedups replays (no dup on the wire) | `tests/netstream.rs::sender_brain_restart_redrive_is_exactly_once` |\n 23│| 4 | **Brain dead across peer connect/disconnect** | Presence consumption pauses | PresenceLog ring buffers liveness transitions | Resubscribe from presence cursor; missed transitions replay | `tests/presence.rs` (both tests) |\n 24│| 5 | **Peer node vanishes (graceful close)** | That conn's streams end | Local broker, other conns, brain | Closed-watcher removes conn row + emits `disconnected`; dial fresh when needed | `tests/presence.rs`; `nethost.rs` closed-watcher |\n   │...\n 26│| 7 | **Registry feed: corrupt record in the stream** | That record only — skipped | The rest of the feed (decoder does not wedge) | Lease makes loss safe; next update supersedes | `replicate.rs::decoder_survives_chunk_splits_and_corrupt_lines` |\n*27│| 8 | **Net disabled / endpoint bind fails at boot** | No WAN; net frames answer `enabled:false` / typed error | PTY hosting (daemon degrades to net-less broker) | Fix config, restart daemon | `broker.rs::dispatch_net_status` (None arm); `Daemon::run` degrade path |\n*28│| 9 | **Broker process restart (journal survives, conns don't)** | Live conns/streams lost | Effect journal (durable) | Deduped op whose resource is gone → typed \"retry with a fresh op_id\" error; brain re-dials; an `spt rc` operator viewport RECONNECTS on its own — centered banner, full establish re-drive inside a bounded window, then a plain-language give-up (REQ-RC-RECONNECT) <!-- [doc->REQ-RC-RECONNECT] --> | `broker.rs::dispatch_net_dial` / `dispatch_net_stream_open` restart arms; `rc.rs` reconne...\n 29│| 10 | **WAN msg feed replayed** (sender redrive, or receiver brain restart resubscribing from a stale cursor) | None — replayed records dedup | Spool state (each op exactly one row); already-delivered ops | Durable `wan_seen` op-id claims absorb the replay; spool path claims atomically with the row | `tests/wanmsg.rs::receiver_restart_replays_feed_without_double_delivery`; `spool.rs` wan unit tests |\n 30│| 11 | **WAN msg record forges its origin** (payload `origin_node`/`node` field) | None — forgery inert (unknown field) | Access-gate subject (handshake-proven `remote_id_hex`) | None needed (records carry no origin field by design — KH 7.5) | `tests/wanmsg.rs::wan_message_lands_exactly_once_under_transport_origin`; `wanmsg.rs::forged_origin_field_is_inert` |\n*31│| 12 | **Target brain killed mid-remote-attach** (operator typing into the dead window) | Pumping pauses until handoff | Broker-held QUIC stream, session + child, output log, effect journal; the operator's viewport | Successor re-serves the same stream from seq 0 (worst case): replayed input dedups at the PTY-write journal; re-transmitted output dedups at the operator's render cursor — every byte exactly once | `tests/attach.rs::attach_survives_target_brain_restart_exactly_once` |\n*32│| 13 | **Target brain killed mid-file-transfer** (push receive: partial temp file on disk) | Transfer pauses until handoff | Broker-held QUIC stream + ring, the partial temp, the durable progress record (last observed position) | Successor re-serves the same stream from seq 0 (worst case full replay): chunks carry absolute offsets — replayed bytes rewrite in place; the commit is an atomic temp→final rename gated on temp completeness, and a replayed commit dedups against an already-committed final (ne...\n 33│| 14 | **Stream lost mid-context-bundle-sync** (responder dies / wire tears before `Done`) | That one pull errors at the requester | The requester's context store (an incomplete bundle is never fetched — length-gated, scratch file deleted); the responder's store untouched | No resume protocol by design: re-pull fresh — the apply is ancestry-idempotent (an already-joined tip short-circuits; fetched tips quarantine under `refs/spt-sync/` so a partial fetch never clobbers `refs/heads/`) | `tests/sync.rs...\n 34│| 15 | **Partition: concurrent context writes on two nodes** | The file's auto-propagation pauses (conflict surfaced, not propagated) | BOTH versions — local file untouched on each node + the other's version as a durable tracked artifact (hazard 6.6); every other file keeps syncing | Elected reconciler (Active instance's node; lowest-id fallback) runs one bounded Psyche turn → merged write `join(vA,vB)+bump` dominates both parents → propagates as a plain accept, clears artifacts subnet-wide | `test...\n 35│| 16 | **Reconcile turn fails** (harness absent / timeout / garbage or empty output / 6.5-suppressed write) | That file stays in conflict-holding state | Both versions (artifacts + local file) — exactly where they were; the store, the sync loop | Retry at the next sync/activation; a fresher direct write re-merges against the new local version | `spt-live reconcile.rs::failed_turn_preserves_everything` / `suppressed_write_preserves_artifacts`; `turn.rs::empty_stdout_is_an_error` |\n   │...\n 41│\n*42│| 22 | **Psyche emits spoofed routing** (`<EVENT type=\"reply\" from=\"evil\" to=\"victim\">` on its stdout) | None — the spoof is structurally inert: the intent parser carries body only (attrs unrepresentable), the daemon re-stamps `from=<psyche_id>` and routes to the inbound message's structural sender (its `from`) alone | The spoof target (receives nothing); the wire (only daemon-authored envelopes); every other agent's identity | None needed (anti-spoof is structural + re-stamp, ADR-0012 / KH 7.3) | `psy...\n*43│| 23 | **Psyche replies with no inbound sender to answer** (alarm-fired turn emits a reply) | That one intent drops with a typed outcome (fail-closed — never broadcast, never guessed) | Every perch (zero deliveries); the notify leg (independent) | The Psyche's next answered turn replies normally | `psyrelay.rs::reply_without_target_is_dropped` |\n 44│| 24 | **Live-Psyche driver loses stdout** (the 7.3 null-stdout shape: detached/`Stdio::null()` regression or a turn that produced nothing) | That turn fails loudly (`TurnError::EmptyOutput`); nothing relays | The relay boundary (no silent success — indistinguishable-from-bug shapes are errors); the spool | Fix the driver / retry the turn | `psyrelay.rs::null_stdout_driver_fails_the_guard`; `spt-live turn.rs::empty_stdout_is_an_error` |\n 45│| 25 | **Runaway Psyche dumps an oversize body** (> 64 KiB in one intent) | That intent drops with a typed outcome | Delivery substrate (never asked to carry it); other intents in the same turn relay normally | None needed (legitimate long bodies chunk at delivery — T3 `EVENT-PART`) | `psyrelay.rs::empty_and_oversize_bodies_drop_typed` |\n 46│| 26 | **Notif feed replayed whole** (re-presented rows after loss/restart — the push-feed's normal recovery) | None — the semilattice join no-ops (`Unchanged` per row, nothing written) | The spool (no duplicates, no state regression) | None needed: replay IS the recovery protocol (full-row re-presentation, no delta bookkeeping) | `tests/notifsync.rs::notif_spools_converge_over_the_wire_and_dismiss_replicates` (replay leg); `notifsync.rs::feed_converges_two_stores_and_dismiss_replicates` |\n   │...\n 52│\n*53│| # | Fault to inject | Expected blast radius | Expected survival | Target |\n 54│|---|---|---|---|---|\n*55│| P1 | **Iroh endpoint task hangs** (not crash — a wedged accept loop) | New inbound conns stall; existing conns + PTY hosting unaffected | PTY side fully isolated (separate threads/runtime) | D9 chaos pass |\n*56│| P2 | **mDNS discovery dies** | LAN dial-by-id fails; direct-addr + relay dials unaffected | Endpoint, conns, PTY | D9 (needs real LAN) |\n 57│| P3 | **Relay unreachable** (n0 outage / air-gap) | WAN dials needing relay fail; LAN/direct unaffected | Everything local | D9 two-host |\n 58│| P4 | **PTY broker drain thread dies** | That session's output stalls | Other sessions, net side | with terminal-wrapper hardening |\n*59│| P5 | **Manifest invocation hangs** (adapter subprocess) | That endpoint's lifecycle op | Daemon scheduler (KH 5.3 timeouts + 7.4 non-blocking) | when daemon hosts N agent loops |\n 60│| P6 | **Registry feed flood** (malicious/buggy peer spams updates) | Bounded by ring caps + lease; CPU cost unmeasured | — | D9 + REQ-SEC-1 outer gate |\n 61│| P7 | **Closed-watcher starvation** (net runtime saturated) | Presence lags behind reality | Conn table eventually consistent | D9 chaos pass |\n 62│| P8 | **Receiver crash between a live-TCP WAN delivery and its op-id claim** | That one in-flight op re-delivers on replay (at-least-once window — chosen over claim-first, whose crash gap silently *drops*; see `wan.rs` module docs) | Spool path (claim+row atomic); all other ops | live-listener durable receipt, if the window ever bites |\n## FLAKE-LEDGER.md\n 15│| 6 | `spt-term tests/stream.rs:49 bounded_backpressure_stalls_an_unconsumed_reader` | 1× hfenduleam Windows (2026-06-15 run 27545857429, PR #14 stale-base CI) | asserts an unconsumed bounded stream stalls the writer-side reader; got `16 -> 82` (reader advanced past the bound) — a timing-sensitive backpressure assert racing the bound check under hosted-runner load. Subsystem disjoint from PR #14 (digest-proof CLI) | OBSERVED 1×: did NOT reproduce on the clean-base rerun (green both runners). Not hard...\n*16│| 7 | `spt-daemon::attach attach_survives_target_brain_restart_exactly_once` | 1× kitsubito (2026-06-16 run 27595180782 attempt 1, M11-W3) | **TIMEOUT, not an assertion fail** — SLOW>60>120>180>TERMINATING, nextest TIMEOUT at the 240s cap (`attach.rs:600`). A heavy multi-process brain-restart-exactly-once test on the slow shared kitsubito box under concurrent load (n1-gate on the same runner + a parallel `ci.yml` docs-publish run firing simultaneously). W3-INDEPENDENT: Windows green end-to-end; ALL tu...\n 17│| 8 | CI build step `Build notify-shell` (kitsubito Linux) | 1× kitsubito (2026-06-16 run 27652755792 attempt 1, v0.8.1 PR #17) | crates.io dep-download blip during the notify-shell build — `download of config.json failed, curl failed` fetching `serde_json`: a transient registry/network fetch failure on the runner, not a code or test fault (a real dep break fails both runners deterministically; this hit one runner once) | observed; cleared by `gh run rerun --failed` (attempt 2 green, builds reused). H...\n*18│| 9 | `spt dummy_harness_e2e` (BOTH tests: `endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches` + `endpoint_run_attach_awaits_online_before_attaching`) | 1× kitsubito (2026-07-05, REMOTE-TRUTH Batch-2 re-gate @f2a799e; roles swap run-to-run) | **process-global `set_var` race in a multi-test e2e binary.** Both tests `std::env::set_var(\"SPT_HOME\", own_tempdir)` then do IN-PROC reads (`perch::spt_home`/`adapters_dir`, `registry::register`, psychebin `fs::copy` staging). One file = one test b...\n 19│| 10 | `spt-store registry::tests::concurrent_registration_never_locks` | 1× hfenduleam local gate (2026-07-05, REMOTE-TRUTH Batch-2 bless matrix @3f6f327, fresh worktree, `nextest --workspace --no-fail-fast`) | 16-concurrent-writer SQLite registration gate FAILED at 5.63s — the SQLITE_BUSY load-contention class ALREADY documented at `db.rs:60`: box saturated by the 1588-test full-parallel suite meant a writer couldn't schedule its retry inside the default 5s `registry_busy_timeout`. Isolated ×5: gre...\n 20│| 11 | `spt-daemon::resume resume_mode_brain_spawning_new_sessions_delivers_each` | 1× kitsubito (2026-07-05 run 28768663321 attempt 1, REMOTE-TRUTH [twohost] PR #52) | Phase-B heavy-class timing EOF — \"session exited before the expected output arrived\" (UnexpectedEof) at 62s under the serialized heavy pool. SAME FAMILY as this file's prior Linux-CI flake (7940318, D4-2b: sequential spawn/read hardening) — a spawned session's exit raced the expected output read on the slow shared box. File untouched...\n 21│| 12 | `spt-daemon::input_ack_deadlock input_flood_through_serve_attach_does_not_deadlock_broker` | 1× hfenduleam local gate (2026-07-06, REMOTE-TRUTH F-2 matrix @a21bc6b, `nextest --workspace --no-fail-fast`) | deadline-margin under full-suite load: the flood exchange's `recv_timeout(30s)` (input_ack_deadlock.rs:512) blew under the 1608-test parallel pool + 7 leaky daemons — FAIL at 34.5s vs ~32s natural runtime isolated (the margin is ~3s by design). Subsystem disjoint from the triggering commit (F-...\n 22│| 13 | `spt::bin cli::tests::probe_all_cap_batches_into_windows` | 1× hfenduleam local gate (2026-07-06, stack matrix @0f08fa9 leg-3 rerun) | fast unit (0.52s natural) FAILED at 1.7s under the 1614-test full-parallel pool — probe-batching window assert with a timing component starved under load; subsystem disjoint from the triggering stack (F-2 reporting / E-2 grid title / picker rider). Isolated ×5: green, 0.52–0.55s. Same night as #12 — the full-parallel local matrix under a busy box is the com...\n*23│| 14 | `spt::bin rc::tests::attach_viewport_reconnects_across_a_broker_bounce` | 1× hfenduleam local gate (2026-07-06, WORKER-TRUTH W-3 build @worker-truth, `nextest -p spt --bin spt` full-parallel 360-test run) | **TIMEOUT, not an assertion fail** — SLOW>60…>TERMINATING at the 240s cap under the 360-way `--bin spt` pool. A REAL-broker + broker-bounce reconnect UNIT test (rc.rs:2071) that escaped the HEAVY-class serialization: the Phase-A/B split + `heavy-broker-pty` test-group target integration te...\n## GATEWAY-LIVENESS-DISPATCH.md\n  6│At the SAME instant, for the SAME pid-alive locally-hosted gateway (mobile-gw, status field ABSENT in info.json, pid alive):\n *7│- **human `spt endpoint list`** → **ONLINE** (renders local perches via `roster::enumerate`, roster.rs:72 = `is_perch_alive` WITH the pid-fallback: status-absent → `is_process_alive(pid)` → alive).\n *8│- **`spt endpoint list --json`** → **Suspended** (renders the endpoint via `resource_projection` over the registry snapshot, cli.rs:2855 → `EndpointRowJson.status` = the registry row status).\n  9│- flynn confirmed mobile-gw IS advertised in all 3 subnet registry files (NOT a re-advertise gap — his earlier \"empty by_id\" was a walker error, retracted).\n 10│\n*11│The adapter's suspend-honor poll reads `--json` (`parse_endpoint_status` over `endpoint list --json --show-all`) → sees Suspended → self-suspends the gateway, despite pid-alive + human-ONLINE.\n 12│\n 13│## Key source facts (already traced)\n*14│- `advertised_status` (registryhost.rs:822-833) DOES feed `effective_rest_state` the pid-fallback `is_perch_alive` (line 825). So the DAEMON's advertised value should be Active for a pid-alive gateway — the divergence is NOT the advertise computation.\n 15│- So the `--json` Suspended comes from the projection/rendering path dropping the pid-fallback the human local-roster applies. **Candidate sites to pin (STEP 1):**\n 16│  1. `resource_projection` (spt-net registry.rs:566) — does it re-derive/filter liveness (e.g. `is_registry_entry_alive`, status-based: liveness.rs:152, which for a status-ABSENT row = not ONLINE||UNBOUND → stale/offline) and DOWNGRADE the row to Suspended/offline, ignoring the advertised Active? (its test `resource_projection_filters_hidden_and_offline` @registry.rs:1821 says it DOES filter on liveness.)\n 17│  2. OR the registry ROW's advertised status is genuinely Active but the `--json` render maps it wrong.\n*18│  3. OR the human path applies a `reconcile_self_owned` local-truth override for locally-owned endpoints that the `--json` path skips (roster.rs:52 mentions reconcile_self_owned overriding stale subnet gossip).\n*19│- **DEFECT A (separate, CONFIRMED, stands):** `establish_perch`'s build (spt/src/api/startup.rs:214-302) carries `cwd/controllable/adapter/read_env` forward from the prior record but NOT `rest_state` → a re-bind writes a fresh InfoJson that WIPES the wake intent (flynn's tick11 rest_state:active vanish).\n 20│- **DEFECT B (RECLASSIFIED to LATENT — do NOT make it the primary fix):** the `status=online` write in `cmd_listen` (startup.rs:486-491) is gated on `live_capable` (manifest `[session.psyche_init]`); a gateway has no psyche_init so it never writes status=online. BUT the pid-fallback is DESIGNED to cover an absent status and DOES for the roster + advertised_status. The operator's ready_agent counterexample proved this: ready_agents work because claude-spt declares psyche_init (live_capable-by-manifest),...\n 21│\n 22│## The fix (after STEP-1 pin)\n*23│**Reader parity:** `endpoint list --json` must report the SAME status for a locally-hosted endpoint as the human `endpoint list` does — i.e., honor the pid-fallback / local-owned truth (`roster::enumerate` / `reconcile_self_owned`) so a machine consumer never sees a different liveness than a human. A pid-alive locally-hosted gateway must read the same (ONLINE/Active) on both surfaces.\n*24│- Fix at the pinned site (most likely: apply the local-roster / self-owned reconcile to the `--json` local-endpoint rows, OR make `resource_projection`'s liveness for a locally-owned row honor the pid-fallback instead of a status-only check).\n 25│- Do NOT reach for DEFECT B (forcing status=online for gateways) as the fix — it risks the seed-#5 orphan-listener falsely-ONLINE amplification; the reader-parity is the correct, minimal fix. (If STEP-1 shows the cleanest fix genuinely is establishing status for a relay-holding listener, flag it back to me before taking it — that's a design call, not a silent choice.)\n*26│- **DEFECT A:** preserve `prior.rest_state` on re-bind in `establish_perch`'s build (same carry-forward discipline as cwd/controllable).\n 27│\n 28│## REQs (mint at build-start, activate-don't-pre-fail)\n*29│- `REQ-LIST-JSON-LIVENESS-PARITY` — `endpoint list` human and `--json` report identical status for a locally-hosted endpoint (esp. a pid-alive status-absent gateway).\n 30│- `REQ-HAZARD-BIND-REST-STATE-CARRY` — re-bind preserves the resting intent.\n 31│\n 32│## Gate (what I'll check)\n*33│- **Regression int/unit:** a pid-alive locally-hosted gateway (status-absent, no psyche_init) reads the SAME status on human `endpoint list` AND `endpoint list --json` — both non-Suspended. This is the load-bearing assertion (the exact flynn divergence).\n 34│- re-bind preserves rest_state (unit).\n*35│- clippy `--workspace --all-targets -D warnings` 0; `traceable-reqs check --json` 0.\n*36│- Full-workspace nextest green (no sibling breakage — this touches the list/projection + establish_perch, shared surfaces).\n 37│- Build in a fresh `.worktrees/` worktree off current origin/main (0825053, W2 just merged), NOT the shared checkout.\n 38│\n 39│Field-verify partner: flynn (mobile-gw). RCA-first — pin STEP-1 site + report before fixing. Qs → doyle.\n## GATEWAY-RCA-STEP1.md\n  5│\n *6│## The pin: `endpoint list --json` skips the self-owned reconcile the human path applies\n  7│\n *8│`spt endpoint list --json` builds its per-subnet rows at **`crates/spt/src/cli.rs:2855-2872`**:\n  9│```rust\n 10│let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new)); // registry-row status\n*11│... EndpointRowJson { status: format!(\"{:?}\", r.status), ... }             // raw passthrough\n 12│```\n 13│`resource_projection` (spt-net `registry.rs:566`) **passes `instance.status` through\n 14│verbatim** (`registry.rs:592`) and only skips non-routable rows (`:582`). It does\n   │...\n 18│The human picker path, by contrast, calls **`reconcile_self_owned`**\n*19│(`crates/spt/src/picker/data.rs:160`, via `gather_endpoints` `:112`): for any subnet\n*20│row whose id matches a LOCAL (self-hosted) perch row, it **overrides the subnet\n*21│row's status with the local roster ground truth** (the `is_perch_alive` pid-fallback,\n 22│`roster::enumerate` → `liveness.rs:136` → status-absent + pid-alive ⇒ ONLINE).\n 23│\n 24│**So for a locally-hosted, pid-alive, status-absent gateway whose snapshot row is a\n   │...\n 26│(ONLINE/Active); `--json`'s subnet section emits the un-reconciled snapshot status\n*27│(Suspended). The adapter's suspend-honor poll (`parse_endpoint_status` over\n*28│`endpoint list --json --show-all`) reads that subnet-section status → self-suspends.\n 29│\n 30│## Candidate elimination (doyle's 3 STEP-1 candidates)\n 31│- **Candidate 1 (resource_projection re-derives/downgrades liveness) — REFUTED.**\n   │...\n 38│- **Candidate 3 (human applies a self-owned reconcile the `--json` path skips) —\n*39│  CONFIRMED.** `reconcile_self_owned` (data.rs:160) is picker-only; `endpoint list\n 40│  --json` (cli.rs:2855) never calls it. THIS is the divergence.\n 41│\n 42│Note: `advertised_status` (registryhost.rs:822) DOES compute Active via the\n   │...\n 49│`REQ-LIST-JSON-LIVENESS-PARITY`: apply the self-owned reconcile to the `--json`\n*50│subnet-projection rows. Concretely — in the `endpoint list --json` builder\n*51│(cli.rs:2855+), for any subnet-projection row whose id is a locally-hosted perch\n 52│(`roster::enumerate` / is_local), override its rendered status with the local\n*53│`is_perch_alive` pid-fallback truth (mirroring `reconcile_self_owned`). Result: a\n*54│locally-hosted endpoint reports the SAME status on human `endpoint list` and\n 55│`--json`. Minimal + does NOT touch the advertise or force status=online (avoids the\n 56│seed-#5 orphan-listener false-ONLINE amplification you flagged).\n 57│\n 58│`REQ-HAZARD-BIND-REST-STATE-CARRY` (DEFECT A, confirmed, independent): preserve\n*59│`prior.rest_state` on re-bind in `establish_perch`'s build (spt/src/api/startup.rs\n 60│~214-302), same carry-forward discipline as cwd/controllable/adapter/read_env.\n 61│\n 62│**Awaiting your greenlight on the pin + fix approach before I build (RCA-first).**\n   │...\n 64│the picker) or be lifted into `resource_projection` itself (broader — every\n*65│projection consumer gets parity, but that couples spt-net to local-perch liveness)?\n*66│Leaning the cli builder (parity where the divergence is, no spt-net→perch coupling).\n 67│\n 68│— todlando\n## INJECT-SETTLE-REARM-GATE-VERDICT.md\n 15│## Both legs GREEN\n*16│- **Windows (ConPTY, isolated worktree + fresh target):** clippy `--workspace --all-targets -D warnings` = 0; `traceable-reqs check --json` = 0; nextest `--workspace` = **1707/1707** (9 leaky, 1 skip), exit 0. `should_settle_rearms_on_observable_pty` PASS.\n 17│- **Linux (kitsubito/forkpty, bundle-deploy @ddf7f8e):** clippy = 0; nextest `--workspace` = **1695/1695** (2 leaky, 1 skip), exit 0. `should_settle_rearms_on_observable_pty` PASS.\n 18│\n 19│## GATE-HOLD investigated → CLEARED (env contamination, not code)\n## KNOWN-HAZARDS.md\n  34│\n *35│### 1.5 Worker (working-perch) lifecycle path consistency\n *36│- **Failure:** subagent-start creates the perch at one path layout; later hooks read it at another → not found; stop-hook scan misses nested perches.\n *37│- **Invariant:** all Worker/Psyche child-perch path composition routes through one central resolver; no divergent path construction across hooks.\n *38│- **spt-core mapping:** `Worker` is a day-one endpoint type; the daemon owns the registry, so perch location is a registry lookup, not ad-hoc path math. Single source of truth for instance→location.\n  39│- **Sister cite:** `src/owl/hook_subagent_start.rs:122-168`; `hook_subagent_stop.rs:15-55`.\n  40│\n  41│---\n    │...\n  46│- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\n *47│- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\n  48│- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\n  49│- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n  50│\n    │...\n  69│\n *70│### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\n *71│- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `...\n *72│- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`inf...\n *73│- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let ...\n  74│- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n  75│\n  76│---\n    │...\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 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 138│### 4.7 Concurrent SQLite openers must not fail with \"database is locked\"\n 139│<!-- [doc->REQ-HAZARD-REGISTRY-CONCURRENT] -->\n*140│- **Failure:** two endpoints on one machine open the same SQLite store at once (e.g. two `ReadyAgent::start` calls registering simultaneously) and one fails outright with `SQLITE_BUSY` / \"database is locked\" → spurious registration/spool failure. Surfaced as a parallel-test flake in `two_agents_exchange_message_tcp_and_spool`, but the bug is real concurrency, not test-only.\n 141│- **Invariant:** `busy_timeout` is set **before** any lock-taking statement on every connection. Switching `journal_mode=WAL` takes a brief exclusive lock; with the default 0ms timeout it fails immediately under contention, so the pragma order is load-bearing: `Connection::open` → `busy_timeout` → `journal_mode=WAL` → `CREATE TABLE …`. WAL alone is insufficient (concurrent *writers* still serialize; they must *wait*, not error).\n 142│- **spt-core mapping:** `spt_store::registry::open_registry` + `spt_store::spool::open_spool_at`; both set `busy_timeout=5000` first. Any future SQLite store (history Path B, instance registry) must follow the same ordering.\n 143│\n    │...\n 145│<!-- [doc->REQ-HAZARD-REGISTRY-EPOCH-LEASE] -->\n*146│- **Failure:** the per-subnet registry replicates `endpoint_id → [instances]` eventually-consistently across nodes. Under a partition or clock skew, a lagging node re-announces a stale `Active` for an endpoint that has actually gone `Offline`. If the merge ordered updates by wall-clock (or \"last write wins\"), the stale `Active` overwrites the newer `Offline` and resolution routes a message to a dead/wrong instance.\n*147│- **Invariant:** the merge precedence key is a **per-node monotonic epoch counter** (`spt_store::epoch::EpochSource`, persisted, strictly increasing, NEVER wall-clock), compared version-vector style per `(endpoint_id, node)`: an incoming update wins **iff its epoch is strictly greater** than the stored one for that node; equal or lower is dropped as stale. So a newer `Offline` (higher epoch) can never be clobbered by a lagging `Active` (lower epoch), and an idempotent equal-epoch replay is a no-op. Wall-...\n 148│- **spt-core mapping:** `spt_net::net::registry::SubnetRegistry::merge_instance` (the lease) + `spt_store::epoch::EpochSource` (the counter). Cross-node replication of the merge wires at D4; the merge seam is identical for local and wire-delivered updates. Chaos/two-host verification = D9.\n 149│\n 150│### 4.9 SQLite stores must create their parent dir — SQLite won't\n 151│<!-- [doc->REQ-HAZARD-REGISTRY-DIR-CREATE] -->\n*152│- **Failure:** `Connection::open` creates the database FILE but never its parent DIRECTORY. On a fresh home (first boot, fresh CI `_work` dir) a registry op that runs before any perch-creating op (`create_dir_all` side effects) fails `SQLITE_CANTOPEN` — \"unable to open database file …owlery\\.registry\". Timing-dependent: whichever code path touches the home first decides the outcome, so it surfaces as a parallel-test flake (bind-first tests losing the dir-creation race to perch-first tests). Bit the h...\n 153│- **Invariant:** every SQLite store's open path `create_dir_all`s its parent dir itself, best-effort, before `Connection::open` — never relying on another subsystem having materialized the home first. (Mirrors the spool, which always did this; the registry didn't.)\n## MANIFEST.md\n   6│\n  *7│**SPT is not a harness.** The manifest declares only what *varies per harness*. Command templates are **opaque strings** — spt-core never parses out a model, tool list, or flag; the adapter writes the full command line and spt-core just runs it with substitution keys filled. Anything spt-core owns (sentinels, spool/registry/perch schemas, the event-block vocabulary, the version-trampoline mechanism, config knobs) is **not** in the manifest.\n   8│\n   9│## What is NOT in the manifest (spt-core-owned)\n  10│\n  11│- **Sentinels** (`.idle`-equiv, echo-gate) — managed by `api state`/`api echo-gate`; the adapter only *calls* them.\n *12│- **Spool / registry / perch `info.json` / daemon-state schemas** — core.\n  13│- **Event-block vocabulary** — the XML tags spt-core surfaces to agents (e.g. `<SPT_EVENT>`, `<owl_messages>`-equivalent) are a **fixed, documented constant**, not configurable. Adapters pass spt-core's output through unchanged.\n *14│- **File-drop filenames** — statically `<endpoint_id>-commune.md` / `<endpoint_id>-signoff.md`. Only the *watched directory* is declared (in `[session]`).\n *15│- **Config knobs** (pulse period, echo-commune window, route-guard window, daily-refresh) — spt-core **global settings**, with optional **per-endpoint override** for endpoints with unique needs. Not per-adapter.\n *16│- **Cross-adapter fallback** (psyche/echo invocation falls back to another adapter on rate-limit) — a **node-wide setting**, not a manifest field, because it references *multiple* adapters (under spt-core, `ccs` is its own adapter, not a binary-swap within one). Adapter-agnostic IDs make this safe — an endpoint's Psyche can run under a fallback adapter temporarily.\n  17│- **Plugin-cache / version-trampoline layout** — none of SPT's concern. A CC-plugin adapter updates via `claude plugin update` (delegated command).\n  18│\n  19│<!-- [doc->REQ-MANIFEST-1] The sections below are the authoritative schema; the\n    │...\n  53│<!-- [doc->REQ-MANIFEST-7] -->\n *54│`shortcut_basename` *(optional, default `spt`)* — the basename the `spt endpoint\n  55│run` picker's `s` keybind bakes into the generated `<basename>-<id>` launcher\n  56│shortcut at the project root (REQ-MANIFEST-7). Absent ⇒ the harness-agnostic\n  57│`spt` (→ `spt-<id>`); an adapter sets it to brand its shortcuts — `claude-spt`\n    │...\n 104│# GO-LIVE GATE ONLY. Declaring [session.psyche_init] is the single signal that\n*105│# promotes the endpoint to a LiveAgent — but spt-core NEVER spawns it (there is no\n 106│# resident Psyche to \"init\"). The per-event turn drives [session.psyche_resume]\n 107│# EXCLUSIVELY (below). The command is opaque and, in the ephemeral model, unexecuted;\n 108│# its presence is the whole contract, so keep it minimal.\n    │...\n 120│```\n*121│The keys spt-core fills into `psyche_resume`: **`{session_id}`** = the Psyche's **own** custody sid (its own thread — never the parent's), **`{parent_session_id}`** = the parent's sid (explicit key), **`{psyche_context_file}`** = the path to the composed-mind file, **`{subnet}`** = the endpoint's home-subnet label. The adapter-static/node keys (`{id}` = the **parent endpoint id**, `{adapter_dir}`, `{adapter_name}`, `{node}`) are also available. (`signoff` is a `psyche_resume`-style variant firing a fin...\n 122│\n 123│<!-- [doc->REQ-PSYCHE-EPHEMERAL-DRIVER] -->\n*124│**Psyche contract — a bounded per-event turn (since v0.25.0).** A Psyche is **not** a resident process the daemon keeps alive between events. Declaring `[session.psyche_init]` is a **go-live gate only** — its presence promotes the endpoint to a LiveAgent, but spt-core **never spawns** it. Each psyche-relevant event (a pulse fire, a commune/signoff drop, a session-custody transition) instead runs **exactly one bounded turn** through **`[session.psyche_resume]` — the sole driven psyche role** — spa...\n 125│\n 126│<!-- [doc->REQ-PSYCHE-SID-CUSTODY] -->\n*127│**Custody sid — `{session_id}` is the Psyche's OWN id.** In a psyche role template `{session_id}` is the **Psyche's own minted session id**, kept in its nested `<parent>-psyche` perch record — **not** the parent's. A parent boundary (`/clear`, `/compact`) rotates the *parent's* sid but does **NOT** rotate the psyche sid: the Psyche's conversational thread survives parent resets (that is its job). When a template still needs the parent's sid it takes the **explicit** `{parent_session_id}` key — neve...\n 128│\n 129│<!-- [doc->REQ-PSYCHE-CONTEXT-FILE-INDIRECTION] -->\n*130│**Psyche-download — `{psyche_context_file}` (file-always, replaces `{psyche_context}`).** The composed Psyche mind rides a **file**, never the command argv: before each turn spt-core writes the mind into the nested psyche perch dir and fills a single **`{psyche_context_file}` = that path** (argv-cap-immune — a real ~20 KB mind exceeds the win32 command-line cap and would brick the spawn). The soft **fresh-vs-continue** discriminator is the file's **content**, not key presence: a **fresh** (first / re...\n 131│\n 132│<!-- [doc->REQ-PSYCHE-NESTED-RESOLUTION] -->\n*133│**`{subnet}` — the endpoint's home-subnet label.** A single `{subnet}` key fills to this endpoint's home-subnet label (`local` when unhomed), supplied whenever it is known so a psyche turn need never resolve a `--subnet` it cannot know; when no subnet is known the key is left unfilled so a referencing template fails **loud** (the `{node}` precedent). There is deliberately **no `{home}` key** — the endpoint home subnet is one concept, and `home:` is already the subnet-name qualifier position, so a `{h...\n 134│\n 135│<!-- [doc->REQ-PSYCHE-SID-CUSTODY] -->\n 136│**Reseed — reserved exit code 95.** When the Psyche's own session is lost or invalid (first host, store wipe, harness-side expiry) the adapter signals it with the **reserved exit code 95**; spt-core clears the stored custody sid and drives the next turn as a fresh preload (a non-empty `{psyche_context_file}`), emitting a loud `PSYCHE_RESEED:<id>` marker so a custody-loss loop is visible — the failure budget above bounds it.\n    │...\n 141│`psyche_init`→`psyche_resume`). spt-core selects `[session.resume]` over\n*142│`[session.self]` **only** when a bringup carries a prior session (`spt endpoint\n 143│run --resume <session>`, or the picker's *Resume from history*) **and** the\n 144│adapter declares the role. An adapter with **no** `[session.resume]` falls back\n 145│to `[session.self]` (full back-compat) — but then a resume re-runs the *fresh*\n    │...\n 152│the resumed PTY in the session's recorded **project cwd** — the per-session\n*153│ledger row's `cwd` → else the perch `info.cwd` → else the current dir (so a\n*154│single-project endpoint and pre-migration ledger rows still resume correctly).\n 155│\n 156│```toml\n 157│# Claude Code: native resume into an existing transcript by id, in its project cwd.\n    │...\n 174│\n*175│**Node-static key — `{node}` (since v0.20.0).** Like the adapter-static keys, `{node}` needs no session/event context, so it is available wherever the session-scoped keys populate (both spawn topologies' `[session.*]` templates) **and** in lazy `[strings]` resolution at `get-string` read time. It fills to **this node's advertised label** — the same value the registry advertises and `spt endpoint list` / the picker render (`LABEL (keyprefix…)`), i.e. the node's OS hostname (read into the label store...\n 176│\n 177│| Key | Fills to | Notes |\n 178│|---|---|---|\n*179│| `{node}` | this node's **advertised label** — the label store value (OS-hostname default, re-checked at daemon startup), OS-hostname fallback when no store label is known | **SINGLE-TOKEN fill only** — tokenize-then-fill cannot produce a space-carrying argv element, so a composite display name like `<id> @ <node>` stays adapter-shim territory (`{node}` complements the shim for tokenizable args, e.g. `--remote-control {id}--{node}`, it does not replace it). **Caveat:** the daemon-side lifecycle reso...\n 180│\n 181│<!-- [doc->REQ-MANIFEST-NODE-KEY] -->\n 182│```toml\n    │...\n 189│<!-- [doc->REQ-NOTIF-2] -->\n*190│`notif` is the **endpoint-native notification render** (ADR-0007's `notif_command` seam): an OS toast, a Shell `alert-symbol`, anything the adapter can run. Declared on harness-adapter **and** shell-adapter manifests; spawned **detached** when a notif surfaces at this endpoint, combinable with the agent-surface delivery (the typed `notify` envelope). Keys spt-core fills: `{notif_id}`, `{notif_from}`, `{notif_subnet}`, `{notif_body}`.\n 191│\n 192│```toml\n 193│[session.notif]\n    │...\n 278│    date-globbed rollout tree. spt-core feeds the extractor only the\n*279│    harness-**neutral** inputs it owns — `{session_id}`, the perch-bound `{cwd}`,\n 280│    and captured `[env] direction = \"read\"` vars — never a harness-specific project\n 281│    slug; the extractor globs the unique `{session_id}` under the root itself.\n 282│    ```toml\n    │...\n 343│Opt-in. The adapter's **idle-delivery translation binary**: a pure stdin→stdout\n*344│JSON-lines filter spt-core lifecycle-manages (spawned when the spt-hosted endpoint\n 345│comes up, terminated when it goes down). spt-core feeds it the inbound `<EVENT>`\n 346│feed and reads back keystroke-commands, which spt-core applies to the broker-held\n 347│PTY **atomically** — controller input is buffered during the emitted sequence and\n## MSG-IDENTITY-DISPATCH.md\n  3│Operator GO (2026-07-10): *\"next milestone: include those two triages plus the most prominent\n *4│backlog seeds, especially bugs that affect messaging and endpoint identity … once sufficient data\n  5│is gathered, find any missing root causes, write the milestone JIT plan, then coordinate with\n  6│todlando and deployah to build → drive to release publish.\"* **F-037 (short msg ids) EXCLUDED by\n  7│operator — covered later.** Test-hardening cluster stays queued (not messaging/identity).\n   │...\n 14│Lanes ([[dont-solo-across-role-lines]]): doyle triage/gate · todlando build · deployah publish.\n*15│One release at the end (target v0.31.0 — new verb = minor, not patch); doyle may split an early\n 16│cut if W4 RCA drags. Field-verify partners noted per wave.\n 17│\n 18│## Waves (build order)\n   │...\n 21│- `REQ-HAZARD-DAEMON-IDENTITY-ENV-SANITIZE` [doc,impl,unit] — daemon scrubs inherited\n*22│  `SPT_ENDPOINT_ID`/`OWL_SESSION_ID`/`SPT_AGENT_ID` at startup + before EVERY role spawn\n 23│  (core defense; today only the role's declared `env_remove` is stripped, runtime.rs:728).\n*24│  Root: perri F-036 RCA — leaked `SPT_ENDPOINT_ID=doyle` through psyche_resume spawns rotated\n*25│  doyle's perch to psyche sids every pulse. Adapter half shipped v0.18.8.\n 26│  **Fold:** the leg-b docs fix — `recursion_guard_env` schema description is role-agnostic\n 27│  (manifest.rs:314 + crates/spt-runtime/manifest.schema.json:306, drop \"summarizer children\").\n 28│- `REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD` [impl,unit] — refuse a bind whose `--set-session-id`\n   │...\n 52│  `is_spt_hosted_no_relay` (inject.rs:37-49) treats a controllable live_agent with no relay\n*53│  address as inject-target → duplicate + stuck-unsubmitted input garbage. Fix = exclude\n 54│  state:live_agent from the inject predicate (perri confirmed core-only). Buildable immediately.\n 55│- `REQ-CARRIER-CLAIM-EXCLUSIVE` [impl,unit,int] — **RCA-first**: operator-observed default-window\n 56│  send to BUSY doyle delivered via BOTH poll AND relay (spool row 156 `taken_leg='idle-inject'`).\n   │...\n 67│### W5 — AUTOSTART (F-038; flynn/mobile-gw, operator-directed)\n*68│- `REQ-ENDPOINT-AUTOSTART` [doc,impl,unit,int] — **shape (a) RULED**: `spt endpoint run --save`\n 69│  persists a startup default replayed at daemon start (symmetric with shipped `subnet attach\n 70│  --save`). Shape (c) restore-what-was-up REJECTED for now (couples to the effective_rest_state /\n 71│  F-035 reader-parity neighborhood that just churned). Loud-skip on failed replay; never blocks\n   │...\n 89│- Branch discipline: build branches off THIS branch (or main once the triage PR merges).\n*90│  Worktrees under `.worktrees/` only. Gates in isolated worktree + throwaway CARGO_TARGET_DIR,\n 91│  nextest, clippy --workspace preflight, `unset SPT_INJECT_VERIFY_ECHO`.\n 92│- doyle gates each wave against this doc + CONTEXT.md/ADRs; RCA locks (W4, W6) come back to\n 93│  doyle for fix-shape ruling BEFORE build.\n## MUSL-RELEASE-HANDOFF.md\n 5│## What's gated (don't re-gate the code — verify the PUBLISH)\n*6│- release.yml musl matrix (kitsubito, `CC_x86_64_unknown_linux_musl=musl-gcc`, in-job musl-tools+cmake+target); assemble globs `spt-x86_64-*` into SHA256SUMS + upload; xtask signs + update-set-enters musl by registry-derived triple (zero xtask code change post-W1).\n 7│- **Proven manually on kitsubito:** static-pie musl binary (no glibc), self-IDs `x86_64-unknown-linux-musl`, live fetch correctly rejects `NoArtifactForPlatform(musl)` PRE-publish. Int test pins update-set select+verify-over-musl-bytes.\n 8│\n 9│## Drive the release","missingPaths":["test"],"truncation":{"content":"# docs/\n## BROKER-BRAIN-SPLIT-RESTORATION.md\n 34|\n*35|1. The hot-swap that *is* wired into `apply` is a **per-session brain handoff** (`apply_brain_only` → `Brain::handoff`), whose job is endpoint survival (REQ-UPD-3), not running-process replacement.\n*36|2. By explicit design, `applyhost.rs:36-38` states: *\"The daemon's own resident loops keep running the old logic until its next restart (`ensure_running` spawns the executable path — now the new binary); that restart is routine lifecycle, never an endpoint cycle.\"*\n 37|3. So new code only goes live on a *subsequent, unrelated* daemon restart (next logon, or a manual restart — which is what made the *other* node, `hfenduleam`, show a new pid). The practical consequence on `enlyzeam`: `gerald`'s live daemon kept running 0.3.0 — including the message-envelope codec — so the box from the original bug report **was still reproducing the original `\\r` corruption even though the fix sat on disk.**\n 38|\n 39|The operator then asked the real question this document answers: **what would it take to make an update actually hand off to the new executable seamlessly — no dropped processes, internal loop timings consistent across the swap, plus any other angles that matter?**\n...\n 50|\n*51|- **broker** (stable kernel) — holds *only* the un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets, and (per the §B ownership table) the Iroh/QUIC endpoint + conn table. Minimal, versioned local IPC. Almost never updates.\n 52|- **daemon brain** (userspace) — all logic (routing, registry, pulse/psyche loops, manifest parsing, update orchestration). Restarts freely on update; rehydrates from disk and re-attaches to broker-held handles.\n 53|\n 54|Explicit evidence the split was meant to be a **process** boundary:\n*55|- ADR-0004 Consequences: *\"A small internal broker **process** exists beneath the daemon — a deliberate, bounded walk-back of 'literally one process,' … guaranteeing endpoint survival across updates.\"*\n 56|- ADR-0004 rejected alternatives include **\"whole-daemon live FD-passing\"** and **\"drain + restart\"** — i.e. the chosen design is specifically *restart the brain, the broker process survives.*\n 57|- `M3a-PLAN.md`, `M3-PLAN.md`, `M3b-PLAN.md` all name \"the broker **process** + IPC (M3b).\"\n 58|- Spikes 01/03/04/05/06 each **PROVEN** with *two separate binaries* (`spt-spikes/spike-01-broker-handoff`): a PTY child + a live QUIC transfer survive 100× brain restarts, gapless and exactly-once. A spike that proves \"survives a brain restart\" presupposes the broker is a separate process.\n...\n 72|\n*73|- The brain cannot restart onto a new binary without killing the in-process broker thread — which would close every PTY, orphan every harness child, and drop every listening socket. So **the no-endpoint-drop self-update pillar is silently unrealized.**\n 74|- `apply` therefore performs an in-process `Brain::handoff` that re-attaches a subscriber within the *same old process* — achieving nothing for a binary swap. `update.rs:234`'s statement that \"the live daemon execs the verified new binary\" is **aspirational; it was never wired** because the process split it depends on does not exist in production.\n 75|- Updates do not run until an unrelated restart/logon (observed live, Section 1).\n 76|\n...\n 78|\n*79|> **Glossary note already applied:** `CONTEXT.md` was sharpened during the session to state explicitly that there is **one broker per machine** (per `SPT_HOME`), *not* one per endpoint — a single broker hosts every endpoint's resources and is present even with zero endpoints online. This corrected a latent \"one broker per endpoint\" misconception and is load-bearing for decision Q2.\n 80|\n 81|---\n 82|\n...\n 86|\n*87|1. no endpoint or harness child process is dropped or suspended (REQ-UPD-3),\n 88|2. internal loop timings stay consistent across the swap,\n 89|3. a failed update fails *safe*,\n 90|4. the mechanism is uniform across Windows and Linux.\n...\n 101|\n*102|**Decision.** The broker process is the long-lived per-machine anchor. It owns the **seed-control lock + liveness** (the single-daemon-per-home invariant and the `ensure_running`/`is_running` ping target) and **supervises/spawns the brain** as its child. On update, the broker re-spawns the brain.\n 103|\n*104|**Reason.** The broker is the one component guaranteed present whenever the machine's daemon is alive — it runs even with zero endpoints (the \"bare daemon\" case, `daemon.rs:196`). The seed lock and liveness *must* survive a brain restart, or a second daemon could win the bind during the restart window and `is_running` would flap. Anchoring them in the never-restarting layer makes the single-instance invariant and liveness survive brain restarts for free.\n 105|\n 106|**Alternatives rejected.**\n 107|- *A thin third supervisor process spawning both broker and brain* — adds a process for no invariant the broker can't already hold.\n*108|- *Brain as parent, spawning the broker* — incoherent: a brain restart would orphan or kill its own broker child, dropping endpoints.\n 109|\n*110|**Operator's challenge that strengthened this.** The operator initially held a \"one broker per endpoint\" model and correctly objected that liveness shouldn't live in something that only exists when an endpoint is online. The code shows one broker *per machine*, always present, so the objection's principle is satisfied and actually reinforces putting liveness in the broker.\n 111|\n 112|### Q3 — Update trigger: brain self-exit, broker auto-respawn from disk\n 113|\n...\n 125|- **Periodic resident loops** (pump cadences, pulse/echo-commune): persist a `(anchor, interval)` pair **once** on a fresh/crash start. The next fire is derived functionally — `next_fire = anchor + interval × ⌈max(0, now − anchor) / interval⌉` — with **no per-fire writes**. An **update restart re-reads `(anchor, interval)` and keeps deriving** (phase preserved, lands mid-grid). A **crash restart is treated as a fresh start** (rewrite `anchor = now`; phase reset is acceptable).\n*126|- **One-shot scheduled events** (alarms): persist the absolute `target-time` **at creation**; every start (update *or* crash) reads it and fires-if-due. **These never reset on crash** — a user's \"remind me at 3pm\" is a commitment that must outlive any restart. **[V3]** The one-shot *rule* is specified this milestone, but the *machinery* is **built with the alarm port, not here**: the daemon has no one-shot consumer today (§7), so building it now would ship untested dead code (matches the project's act...\n 127|\n 128|The brain distinguishes update-restart from crash/fresh-restart via a signal on the broker's respawn (the broker *initiated* the planned cycle but *observed* the unexpected crash). The `(anchor, interval)` write happens once per fresh/crash start, zero per update, zero per fire.\n 129|\n...\n 169|\n*170|**Decision.** If the new brain fails to reach a **ready** signal (re-attached all sessions + resumed loops) within a bounded number of boots / a healthy-run window, the broker **rolls back to the last-known-good binary** (the `spt.exe.old-N` backup), **quarantines** the bad version (no auto re-apply/re-fetch), and surfaces a loud consent-style notification. The broker holds **last-known-good + candidate** paths and chooses which to spawn (no file manipulation at failure time). Endpoints survive throughou...\n 171|\n 172|**[V1] Rollback-state-compat invariant (forward invariant, mint this milestone).** Auto-rollback spawns the **old** binary against durable state the **new** brain may already have written. This is safe *today* (a source audit confirmed zero state-migration code exists), but the first release that migrates a durable-state schema would silently break Q7's rollback. The invariant must be minted now, while it is free: **a brain must not irreversibly migrate durable state before ready-promotion** — equivale...\n 173|\n 174|**Reason.**\n*175|- The dangerous case is a *runtime* bug (new brain panics on boot), not the binary land (already rolled back at `applyhost.rs:167-170`) and not IPC mismatch (the `classify` handshake `brain_ipc_version ≥ broker.min_compatible` is checked pre-swap). A naive supervisor would crashloop the new binary: endpoints alive but logic dead — \"up but useless.\"\n 176|- The backup binary is already produced for exactly this, and the readiness+backoff machinery already exists — `peerloop.rs:82-91` has `SUPERVISE_BACKOFF_BASE/CAP` + `SUPERVISE_HEALTHY_RUN` (60 s healthy resets backoff), and `supervise_pump` is wired at `peerloop.rs:805` (M8-D4, verified). A bad update that bricks *logic* should self-heal back to last-good without a human, the same spirit as the boot-race self-heal already shipped.\n 177|- The two-phase record fixes a real existing bug: `applyhost.rs:176` writes `record_applied(version)` + `last-outcome=applied` **before** the brain handoff — i.e. before we know the new brain boots. (This is the optimistic `applied.json={version:6}` observed on `enlyzeam`.)\n 178|\n...\n 192|- A new hidden `spt daemon brain` entry is the **brain** process: connects to the broker, runs pump/dispatch/psyche loops/session drivers, rehydrates from disk, emits a `ready` signal.\n*193|- `ensure_running` / `is_running` / `daemon stop` contracts are unchanged — they target the broker's seed channel.\n 194|- The de-elevation guard (`daemon.rs:60-103`) applies at the **broker** entry; the brain child inherits the unelevated identity.\n 195|\n 196|---\n...\n 199|\n*200|This is **milestone-sized**, not a patch. It includes: the process decomposition; an IPC-verb surface for the hard cases (digest push, seed-lock/liveness, plus the general direct-call audit — net bring-up is near-free per the §Q5 audit correction); broker-side per-session delivered-cursor tracking; broker-owned generation custody handed at spawn **[V2]**; durable absolute-deadline timing across **phase-significant** resident loops only **[V4]** (the idempotent pump cadences need *no* conversion — co...\n 201|\n 202|### 6.1 Sequencing — RATIFIED (operator, 2026-06-09)\n 203|\n...\n 215|\n*216|- **Alarms have no durable daemon scheduler.** In the spt-core daemon, `alarm` exists only as an event *shape* (`spt-proto`) + relay handling (`psyrelay.rs:93`) + a test fixture; `target-time` is only a wire attribute. The actual \"wait until target, fire the TIMED PULSE\" timer lives in the legacy owl listener in-memory. Porting alarms into the daemon as a durable scheduler (which would then ride the Q4 one-shot mechanism) is its own gap.\n 217|- **Registry one-cadence reconverge window.** `RegistryHost` is intentionally in-memory (`registryhost.rs:19-26`): a restart forgets peers' rows and reconverges within one pump cadence (self-rows re-advertised each tick; mirrored to `identity/registry/` JSON for out-of-process readers). Across a brain restart this is a brief reconverge window, accepted by design — not a blocker, noted for completeness.\n 218|- **Multipart inter-agent message truncation — belongs to LEGACY `claude_skill_owl`, not spt-core.** During the verification exchange (which ran over the legacy live-agent messaging infra — the `owl.exe` listener, not the spt-core daemon), an 18-part chunked message reached the recipient as ~parts 1–8; a long resend also dropped its tail. Long messages silently lose their tail — a delivery-integrity issue in the chunk-reassembly path. **This is a legacy-project bug** (the agents in this exchange ...\n 219|- **Stale DEFERRED mesh row.** Side-flag from verification: the mesh row in `docs/DEFERRED.md` is stale (mesh shipped at v0.3.0). Minor cleanup, noted for a docs pass.\n...\n 225|- **ADR-0004** — `docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md` (the decision being restored; §A update classes, §B ownership table, the spike amendments).\n*226|- **Requirements** — REQ-UPD-3 (no-endpoint-drop, brain-only), REQ-UPD-4 (staged + consent), REQ-UPD-6/8 (update-set / platform-safe), REQ-DAEMON-3/9 (auto-start / net self-heal), REQ-HAZARD-UPDATE-ROLLBACK, REQ-HAZARD-HANDOFF-ARGV-COMPAT.\n 227|- **Glossary/model** — `CONTEXT.md` daemon / broker / brain (incl. the one-broker-per-machine clause added this session).\n 228|- **Spikes** — `docs/spikes/SPIKE-01-broker-handoff.md`, `-03-quic-survival`, `-04-forkpty-parity`, `-05-restart-stress`, `-06-idempotent-boundary` (proofs that the two-process handoff works on both OSes).\n 229|- **Key source** — `daemon.rs` (`run`, net consumers, self-heal, digest hub, shellwake, seed-control), `broker.rs` (`OutputLog`, `bind`, `EffectJournal`), `brain.rs` (`BrainState`, `cold_start`/`handoff`/`snapshot`), `applyhost.rs` (`apply_staged`, `connect_retry`, record-before-handoff), `update.rs` (classes, `classify`, `apply_brain_only`), `peerloop.rs` (cadences, supervise-backoff), `lifecycle.rs` (pulse loop), `registryhost.rs`.\n...\n 238|- **`REQ-*` registry mints** (registry-first, `traceable-reqs.toml`; minted INACTIVE per rule 5, activated per `RESTORATION-PLAN.md`'s schedule):\n*239|  - **`REQ-HAZARD-BROKER-PROCESS-ISOLATION`** — a brain restart must never drop a hosted endpoint (REQ-UPD-3's real promise). **Evidence [V5]:** an `int` stage productionizing SPIKE-01/03 — a PTY child + a live QUIC conn survive a brain-*process* restart onto a *swapped* binary. REQ-UPD-3 / REQ-DAEMON-2's current `int` evidence (`applyhost.rs:39/77`, `update.rs:246`, `brain_swap.rs`) proves only the *in-process* handoff shape — **mis-evidenced today, re-pointed at plan task D7.**\n 240|  - **`REQ-HAZARD-ROLLBACK-STATE-COMPAT` [V1]** — a brain must not irreversibly migrate durable state before ready-promotion (pre-ready writes stay N-1-readable).\n 241|  - The functional two-process-split realization + the **[V6]** N-1 old-broker × new-brain CI compat test are carried as `RESTORATION-PLAN.md` tasks D1–D7 (the compat test lands as a D7 gate).\n 242|- **KNOWN-HAZARDS** §6.7 (in-process collapse) + §6.8 (rollback-state-compat), with condensed-checklist rows.\n## CONTEXT-MEMORY.md\n 6|\n*7|- **live context** (per-agent, project-independent — the agent's *mind*): identity/persona, relationship + rapport with the user, the user's cross-project preferences & working style, cross-project life threads, **learnings from work that may apply to other projects**, a thin **project roster** (one line per project the agent works on), the agent's self-model. Synced to **all** instances of the endpoint.\n 8|- **project context** (per-agent-per-project — what the agent is doing *here*): codebase/architecture, decisions made here, **current focus / next steps / todos**, project conventions, build/test commands, blockers. Synced **only** to same-project instances.\n 9|\n 10|(This split independently matches pi-agent-memory's Project Store vs Global Store and its discriminator dialogue — convergent validation.)\n...\n 30|- **`/compact`** → resume Self → **commune** file-drop.\n*31|- **`spt suspend`** → resume Self → **signoff** file-drop (suspend = the endpoint going down, so it's a signoff, not a commune).\n 32|\n 33|This deliberately swaps the project-centric echo for a Self-authored capture at exactly the moments that matter (context loss + suspend), so live context stays current. (Routine cadence communes remain echo — project-primary, live-conservative.)\n 34|\n...\n 36|\n*37|A lever for an agent behind an **spt-hosted** endpoint to clear and resume itself without stalling. It: (1) triggers a `/clear` (which uses the **Self-resume-commune rule** above — consistent with all other boundaries), (2) thereby captures a commune, and (3) **guarantees a resume signal hits the harness post-clear** so the agent's turn restarts and it resumes work — starting from its immediate next-steps. Spt-hosted-only because the guarantee requires the daemon to own the PTY and inject the post-cl...\n 38|\n 39|### Ownership\n 40|\n...\n 73|\n*74|A generated, lightweight **INDEX** over topics — summaries + concepts + aliases + paths — **not** a vector DB, **not** a full mirror (adapted from pi-agent-memory's Retrieval Map). One INDEX per **project** (aggregating all agents' project context → realizes cross-agent synthesis + query-routing: \"which endpoint owns this task\") and per **agent**. Regenerated on context change; the merge driver + INDEX regeneration keep it current.\n 75|\n 76|## Start-injection: dense working set, sparse tail\n 77|\n...\n 82|- **Hot/cold curated by the Psyche** via `Pinned` + recency; keeping the hot set tight is part of maintaining memformat.\n*83|- **Tunable** via a working-set **density knob** (spt-core global setting + per-endpoint override).\n 84|\n 85|## Deferred\n 86|\n## DEBUG-CONVERGE-PLAN.md\n 19|After a coordinator stages a debug rollout, answer one question without\n*20|hand-walking nodes: **did every expected lab node converge on the target debug\n 21|version, and if not, why not?**\n 22|\n 23|`debug-converge` watches until every *expected* debug-pinned reachable node\n*24|reports the target version applied, or it returns a per-node timeout table.\n 25|This is what turns \"fast to stage\" into \"fast to *verify deployed*\" — the\n 26|actual deployment-speed win the rollout pipeline exists for.\n 27|\n...\n 43||---|---|---|\n*44|| `--version <u64>` | required | target debug version to converge on |\n 45|| `--nodes <id,…>` | derived | explicit expected-node set; overrides derivation |\n 46|| `--subnet <name>` | the rollout's lab subnet | scope derivation to one subnet |\n 47|| `--timeout <secs>` | 120 | overall watch budget |\n...\n 55|still *expected* and shows up as a row, not a silent omission. `--nodes` gives\n*56|an explicit set for targeted tests.\n 57|\n 58|## Per-node state model\n 59|\n...\n 65||---|---|---|\n*66|| `Applied` | node reports `current debug version == target` | ✓ success |\n 67|| `NotPinned` | node is on the `stable` channel — rejected at the metadata gate | ✓ (expected-set bug or intentional) |\n 68|| `Offline` | unreachable within the round | pending → timeout |\n 69|| `StagedAwaitingConsent` | candidate staged, default-gated, no apply ack yet | pending |\n*70|| `BlockedByBrokerResources` | broker-touching candidate refused while live endpoints held | pending (operator must quiesce) |\n 71|| `Rejected{reason}` | typed refusal: `NoArtifactForPlatform`, `WrongChannel`, `Rollback`, `ArtifactMismatch`, … | ✓ failure |\n 72|\n 73|Convergence = every expected node is `Applied`. Any node still pending when the\n...\n 102|```\n*103|DEBUG_CONVERGE target=7 channel=debug subnet=lab expected=3\n 104|  node-a   Applied                v7\n 105|  node-b   StagedAwaitingConsent  v6  (run: spt update apply)\n 106|  node-c   Rejected               NoArtifactForPlatform (x86_64-apple-darwin)\n## DEBUG-ROLLOUT.md\n 15|- A rollout is a signed `SignedUpdateSet`: one signed metadata record, with one\n*16|  artifact digest per Rust target triple.\n 17|- Each recipient verifies the set under its node-local `release-keys.json`,\n 18|  selects only its own platform artifact, stages it, and then follows the normal\n 19|  consent/apply policy.\n...\n 25|- A broker-touching candidate may be staged, but apply refuses it while\n*26|  broker-held runtime is live. Shut down or suspend hosted endpoints first; the\n 27|  first flow does not auto-cycle them.\n 28|\n 29|## One-time lab setup\n...\n 63|- `--build-current` builds and includes the coordinator's current platform.\n*64|- `--artifact <target=path>` adds an already-built artifact for another target,\n 65|  such as `x86_64-pc-windows-msvc` or `x86_64-unknown-linux-gnu`.\n 66|- `--version <u64>` overrides the local debug sequence. Use a higher value when\n 67|  recovering from lost local state.\n...\n 69|- `--state <path>` stores the local debug sequence somewhere other than\n*70|  `target/debug-rollout-state.json`.\n 71|\n 72|The staged update then propagates through the normal pull-based update pump.\n 73|There is intentionally no bespoke push path.\n...\n 96|`Applied` / `StagedAwaitingConsent` / `NotPinned` / `Rejected{reason}` /\n*97|`Offline` — exiting `0` only when every node applied the target (`1`\n 98|otherwise, `2` on usage). Spec + state model: `docs/DEBUG-CONVERGE-PLAN.md`.\n 99|The manual walk below remains the fallback.\n 100|\n...\n 103|- `NoArtifactForPlatform` means the signed set did not include the recipient's\n*104|  Rust target triple.\n 105|- `WrongChannel` means the node is not pinned to `debug`.\n 106|- `Rollback` means the offered debug version is not greater than the node's\n 107|  current debug-channel version.\n 108|- `RefusedClass(BrokerBreaking)` means the candidate touches broker-held\n*109|  resources; quiesce hosted endpoints first.\n 110|\n 111|## Agent checklist\n 112|\n## DEFERRED.md\n 10|| Concrete Shell types (GameRobot, in-session-inject) — **OS-notification shell carved out: ships M5** as the dogfood proof (user decision 2026-06-04, M5-PLAN §Scope decisions) | Shell concept v1 | Model is locked; concrete surfaces are large, platform-specific, and not needed for core | First real use case for a driven surface (e.g. the 2D-world experiment) |\n*11|| PresenceChannel implementation (broker, dispatch/bind/thread) | endpoint types v1 | Depends on presence gossip + Shells + multi-instance routing; only the seams ship v1. **M5 ships presence *resolution* (gossip + most-recently-active API) — its future substrate; the endpoint stays deferred** (user decision 2026-06-04). <!-- [doc->REQ-PRES-1] --> **M5-D6 seam note:** the substrate now exists — the gossiped datum (`Instance.last_active_ms`, riding the registry's epoch lease + replication) and the one...\n 12|| OS-level input-activity presence signal (7a-A) | presence v1 | Per-platform, privacy-loaded; agent-interaction heartbeat covers the v1 goal. **Note:** distinct from the v1 broker *sensing* user input on a PTY it already holds (spt-hosted) — that is in v1 and is NOT OS-wide input monitoring | Opt-in enhancement after agent-interaction presence proves out |\n 13|| Instantiate-anywhere + consent gate | Instances V1-mid | Remote launch over network is additive once remote-drive of running instances works. Consent *model* is now designed (CONTEXT §Consent & security gates); **the framework (grant store + escalation + pre-consent flags) ships M5 with this capability id reserved-but-refusing** (user decision 2026-06-04) — the capability lands into it later. Defers with it: the remote-fork arm, cross-node shell spawn, `shell_wake_spawn_anywhere` *behavior* (grant s...\n 14|| Remote command execution on another node | off-node reach-back v1 | Highest-risk capability; needs the consent/security gate. Workarounds exist (message a local agent; interact with local agent directly). **M5 ships the framework with this capability id reserved-but-refusing** (user decision 2026-06-04) | Designed alongside instantiate-anywhere's consent gate |\n 15|| Shell-binary (and harness-binary) sandboxing | Shell concept v1 | A capability toolset bounds what the *agent* can ask, not what the *binary* can do with its OS perms. Baseline stance: running any adapter/shell binary = a disclosed, accepted trust risk for all spt-core users | If/when untrusted third-party shell/harness binaries become common enough to warrant OS-level sandboxing |\n*16|| Concrete Shell adapters (e.g. GameRobot) + the shell binaries themselves | Shell concept v1 | The Shell *model*, manifest schema, perch layout, and `api` surface are now specified; concrete shells are large, platform-specific, and not needed for core | First real driven-surface use case |\n 17|| REQ-CONSENT int evidence (interactive escalation E2E) | M5 closeout (D9b rule-5) | The escalation prompt''s end-to-end leg needs a real harness session answering a real consent prompt — loopback-faking it would prove nothing; the grant store + gate + CLI are impl/unit-proven | The downstream rebuilt spt plugin (v1 acceptance) — its first gated spawn IS the int evidence |\n*18|| REQ-INSTALL-4 int evidence (`spt adapter add --github` against a real repo) | M5 closeout (D9b rule-5) | Needs a real clone target; impl/unit cover manifest-first validation + registration. The standalone `SaberMage/spt-shell-notify` repo now makes this trivially possible | First milestone that touches adapter lifecycle again — one E2E cloning the notify repo |\n 19|| OS-service registration at install (REQ-INSTALL-1's third leg: systemd user service / Windows service or scheduled task for the always-on guarantee) | installer v0.1 (M6-D2, grill decision 3) | Daemon auto-start on any `spt` invocation covers dev-stage use; the minimal non-interactive script stays non-OS-entangled (also serves REQ-INSTALL-2's repackaging stance). Honest gap: a node is unreachable after reboot until something invokes `spt` | First always-on deployment need (headless/server node, or the ...\n 20|| Tier 2 agent-docs: MCP doc/resource server + `spt <cmd> --help --json` structured-help mode (REQ-DOCS-4 legs) | docs v0.1 (M6-D3/D5, DOCS-STRATEGY §v0.1 grill) | Tier 1 covers the integration surface: the schemars-derived manifest schema + llms.txt/llms-full.txt + the generated CLI reference; `--help --json` is low marginal value over that reference, the MCP server is the standout *later* fit | First integrating dev-agent that outgrows llms.txt + schema (or the `spt-claude-code` work surfacing a concr...\n 21|| Subnet attachment verbs: `spt subnet detach <NAME> [--auto]` / `attach <NAME> [--auto]` (daemon keeps running, stops/starts advertising + connecting for that subnet; `--auto` persists the startup default), `spt subnet leave <NAME>` (elevation-gated), maybe `spt subnet disband <NAME>` (elevation + current TOTP) | M7 grill (user spec 2026-06-06; CONTEXT §subnet attachment) | The attached/detached *term* + the all-attached banner ship M7; the per-subnet serve-state machinery (selective advertise/connect,...\n...\n 26|| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n*27|| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite ho...\n*28|| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\n 29|| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache...\n 30|| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n*31|| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`w...\n*32|| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version ...\n 33|| **Instant + never-seen peer hostname via pairing-time capture (REQ-SUBNET-3 follow-up; the deferred \"option 1\")**: the shipped node-level label (option 2) is GOSSIP-ONLY, so a peer's name is learned only from a pump round while it is alive. Two residual gaps remain after the eviction fix below: (a) ~1 gossip cadence to appear after a join (the joiner trust-pins by pubkey with `label=None`; the hostname arrives next pump round — enlyzeam↔kitsubito converged to `ENLYZEAM` after a cadence, fine but no...\n*34|| ~~Subnet-scoped liveness probe (REQ-SUBNET-5 detach gap)~~ **SHIPPED 2026-06-08** (REQ-SUBNET-5 int stage): `subnet status --nodes` showed a DETACHED peer as **online** because liveness fell to `Probe` → a raw transport dial by node-pubkey, which succeeded on the peer's still-bound daemon endpoint (one ALPN `spt-core/net/0` serves all subnets, so the dial was subnet-blind). FIX: a new `ServeProbeRecord` (spt-net `serveprobe`) + daemon `serveprobe` handler/requester ask the peer \"serving subnet X?\", a...\n 35|| **Subnet full-mesh visibility (PLANNED MILESTONE — `SUBNET-MESH-PLAN.md`)**: non-directly-paired members never see each other — a subnet is currently the PAIRING GRAPH, not a mesh (A↔B, C↔B; B offline → A and C invisible; even with B online, B doesn't relay). Root: pairwise TOFU trust (`trust.rs`) + own-rows-only gossip / no transitive relay (`peerloop.rs`, KH 4.10/7.5) + no member roster in the pairing seed transfer. FIX = member-AUTHENTICATED subnet data (seed-MAC and/or per-node signatures...\n*36|| `xtask debug-converge` — automated convergence watcher for debug rollout (the REQ-UPD-6 first-slice item ADR-0016 names): watch every expected debug-pinned reachable lab node until all report the target version applied, else a per-node timeout table (Offline / StagedAwaitingConsent / BlockedByBrokerResources / Rejected{reason}). Activates REQ-UPD-6's `int` stage. Full build plan: `docs/DEBUG-CONVERGE-PLAN.md` | REQ-UPD-6 first slice (2026-06-06) | The first slice ships manual \"Apply and observe\" (`DE...\n 37|| **Origin-source update bootstrap (`spt update fetch` — pull the latest signed release from GitHub into the existing verify→stage→apply pipeline)**: today update DISCOVERY is **peer-only** (REQ-UPD-1: a daemon pulls offers from roster peers via `request_update`; `propagate.rs`/`peerloop.rs`). There is NO origin fetch, so the **first node in a fleet — or any isolated/solo node — cannot get a new release without a peer that already staged it** (the fleet converges by gossip, but only *after* a m...\n 38|| **Test broker socket-bind hardening (CI-flake removal)**: `applyhost` tests' `served_broker` helper does `Broker::bind(name).expect(\"bind broker\")` (`crates/spt-daemon/src/applyhost.rs:293`) where `name` = `spt-daemon-d7b-{pid}-{seq}.sock`. Flakes intermittently on the **kitsubito** self-hosted Linux runner (likely AF_UNIX path collision / stale socket file under the runner's reused workspace — Windows named pipes don't hit it). Cost the v0.3.2 release a full red CI run + a `--failed` rerun (2026-06-...\n 39|| **`spt update fetch` single→set shape-upgrade at same version (REQ-UPD-8 transitional trap)**: `cmd_update_fetch` sets the rollback floor = `staged_version` (the monotonic counter), and the gate is a plain `candidate > floor`. A node that fetched+staged version N as a platform **single** (pre-0.3.2 fetch behavior) can never re-fetch version N as a multi-platform **set** — same counter → `Rollback{current:N, candidate:N}`. Bites the **seed-node bootstrap**: the first node to origin-`fetch` a relea...\n...\n 42|| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes, by bounding the pump's brain-IPC reads and escalating a TimedOut to a supervised restart. But the ROOT cause is broker-side: the broker's `net_open_stream` / `net_stream_send` / `net_dial` handlers m...\n*43|| **Durable in-daemon alarm scheduler (one-shot deadline machinery — the Q4/V3 deferral)**: the broker/brain restoration (ADR-0018 Q4) fixes the *rule* for one-shot scheduled events — persist the absolute `target-time` at creation; every brain start (update *or* crash) reads it and fires-if-due; **never reset on crash** (a user's \"remind me at 3pm\" is a commitment that must outlive any restart). But it deliberately does **not** build the machinery: the spt-core daemon has **no one-shot consumer today...\n*44|| **Autonomous session-digest freshness + Option-C re-home (M9 ADR-0008 amendment deferral)**: M9 re-founded the digest as an ON-DEMAND projection of normalized session logs (`spt-term::projection`; `spt-daemon::digest::project_endpoint_digest`) — snapshot-pull and the structured-delta-stream CONTRACT ship, deltas driven by pulls / `api digest-entry` pushes. Two pieces deferred, both because they have **no consumer yet** (Shell=M10, GUI/frontend pane, Gateway agent-window — none exist): (a) the **aut...\n 45|| **Two-origin `owl_message` context-tap subtype refinement (M10 ADR-0019 deferral)**: the two-origin merge's owl-message producer (`spt::api::startup` `deliver` closure) taps the live-agent **`emit` chokepoint**, so EVERY delivered frame — including resurfaced **notif** frames and shell-context rows — is recorded as a single coarse `owl_message` context-injection entry (`context_kind:\"owl_message\"`). The data is not wrong (a notif IS injected context), only coarsely labeled; the digest is a glanceab...\n*46|| **W2.5 — attached-presence + Kick-and-attach (M12 picker blue tri-state)**: M12 W2's picker ships online/offline status only; the **blue ■ \"attached\" tri-state** + the **\"Kick `<node>` and attach\"** confirm option were SCHEDULED (operator 2026-06-14, NOT dropped) as a dedicated slice built AFTER W2 lands, then wired back into the picker. Scope: (1) a **broker attach-presence query** — \"who is attached to endpoint E's PTY, by which node/surface\" — derive from the `presence.rs` PresenceLog conn c...\n 47|| **Cross-node shell TUNNEL relay (REQ-SHELL-4 cross-node-on-LAN carry)**: W3 built + proved the opaque byte tunnel **same-node only** (broker-homed `TunnelHub` loopback stream pair + the local control socket; `tunnel_e2e` round-trips `<EVENT`-looking bytes byte-exact). The **cross-node** path does NOT exist: `StreamFamily` (dispatch.rs) has no `Tunnel` variant, `classify_first_line` no tunnel arm, `SHELL_LINK` carries only relink/cmd/drive (no tunnel action), and every tunnel surface (`tunnel_ensure/sen...\n 48|| **Per-connection contiguous-sent cursor — structural guard against a decreasing-floor controller re-take (`REQ-HAZARD-CONTROLLER-WRITER-REORDER` follow-up)**: v0.13.0 P1c fixes the controller-writer reorder while KEEPING handoff's eager subscribe (fix #1 \"drop the subscribe\" was reverted — it's the standalone-resume mechanism), so the decreasing-floor double-take (handoff subscribe@K + serve_attach subscribe@0) is still PRESENT and made safe by: `controller_writer` epoch-gated under `send.lock()` (...\n*49|| **Adapter-manifest-enabled rc VT/mouse mode (`spt rc` scroll vs right-click-paste xor resolution)**: v0.13.0 P1/P1b's Windows `EnableMouseCapture` (for client-originated right-click bracketed paste, 7.18) makes Windows Terminal hand ALL wheel events to rc instead of scrolling its own scrollback; rc forwards the wheel to the harness as SGR reports (7.20) but ONLY when the harness has mouse-reporting on (`enabled && sgr`) — and **Claude Code's REPL never enables mouse tracking** (it has no internal scr...\n*50|| **Always-on endpoint kind + `#`-sigil addressing (ADR-0023; REQ-EP-8 / REQ-EP-9) — the core seam for adapter-bridged group/forum messaging**: a `[always-on]` manifest section declaring a resident, owner-less, addressable endpoint binary the daemon boot-launches + supervises **continuously** (independent of agent liveness, register-triggered, one per `<adapter>[:profile]`), which self-manages its `#`-addressed channel endpoints via the existing `api bind` and may call the already-ungated `endpoint wak...\n## DOCS-STRATEGY.md\n 27|\n*28|## Killer quickstart targets (two, one per audience)\n 29|\n 30|- **Human dev:** two agents exchange a message end-to-end in **one command + minimal config**, < 10 min — the core primitive, whole-thing-first, then decomposed.\n 31|- **Dev-agent integrator:** a minimal **adapter** that satisfies the manifest + `api` contract and drives one lifecycle operation — the \"build a harness for spt-core\" hello-world.\n...\n 83|- [Quickstart: build an adapter](…/quickstart/adapter.md): minimal manifest + `api` contract.\n*84|- [Mental model](…/concepts/overview.md): daemon, endpoints, instances, subnet.\n 85|\n 86|## Capabilities      (one bullet per vertical, each → its own llms.txt)\n 87|- [Messaging](…/messaging/llms.txt): typed ops + file blobs; send / ring / ready.\n## DORMANCY-BUDGET.md\n 25|   per-seat overhead: PTY plumbing + output rings);\n*26|5. kills every seat (the **suspended** state — only the on-disk perch record\n 27|   would remain in production) and samples the residual.\n 28|\n 29|Sampling: Windows via `Get-Process` (WorkingSet64 / HandleCount /\n...\n 63|residual: host process returns to its ~6–7 MiB baseline — a suspended seat\n*64|costs **only the on-disk perch record** (bytes, not megabytes, no handles, no\n 65|CPU).\n 66|\n 67|### gravity (Ubuntu 22.04, 2026-06-04, `[budget]`-tagged CI run @ 785002e)\n...\n 91|   amended. The knob chain shipped in D9-2 (`auto_suspend_after_ms`: global\n*92|   default OFF → `daemon.json` node leg → `info.json` endpoint leg, `0` =\n*93|   explicit endpoint OFF) is the policy surface.\n 94|3. **Constrained nodes should default the node leg ON.** ~300 MiB/seat is\n 95|   real on a Pi/handheld-class node: set `auto_suspend_after_ms` in\n 96|   `daemon.json` there. The threshold counts from **dormancy onset** (the\n 97|   `dormant_since_ms` anchor), never from last activity.\n*98|4. **Heavy-harness endpoints may opt themselves out/in per seat** via the\n*99|   endpoint override — e.g. a billable harness whose warm session holds a\n*100|   cost/footprint beyond RSS (seat-count limits) sets a per-endpoint\n 101|   threshold even on a big node. Note: an **idle** claude seat holds no API\n 102|   spend (tokens flow only on input); the footprint concern there is seat\n 103|   count and memory, not per-minute billing.\n## F-027-ENDPOINT-SPAWN-FAIL-DESIGN.md\n*1|# F-027 — ENDPOINT_SPAWN_FAIL: pre-spawn seam + bind-deadline honesty (DESIGN)\n 2|\n 3|Status: **FROZEN 2026-07-03 — field review complete.** Author: doyle. Inputs:\n 4|perri field seeds + spawn-fail taxonomy (2026-07-03, 16-part dispatch,\n...\n 18|\n*19|`spt endpoint run <id> --start` prints `ENDPOINT_RUN_STARTED` when the broker\n 20|has accepted the spawn RPC and a child PID exists — **nothing more is verified**\n 21|(cli.rs:1819 machine line, cli.rs:1833 `--start` success print). A harness that\n 22|launches but wedges post-spawn (workspace-trust prompt, null stdin) never fires\n*23|its SessionStart hook, so `establish_perch` never runs (api/startup.rs:299 is\n*24|the ONLY writer of the `ready` marker), and the endpoint sits `ready=false`\n 25|**forever, silently** — while the operator was told it started.\n 26|\n 27|`ready=false` is already rendered by `spt list`/picker (roster.rs:69 →\n 28|cli.rs:2735/3826) but nothing gates or alarms on it: passive display only. The\n*29|attach path's `await_endpoint_session` (cli.rs:1854, 25 s) gates on broker\n 30|**session existence** — created synchronously at spawn, so it passes instantly\n 31|for a wedged harness — and the `--start` path skips even that.\n 32|\n...\n 35|KNOWN-HAZARDS F-019 lesson). Any F-027 token must land on **CLI stderr and/or\n*36|the perch record**, never daemon-stderr-only.\n 37|\n 38|## 2. Field evidence (perri, 2026-07-03 — provenance table)\n 39|\n...\n 41||---|------|----------|-------|\n*42|| 1 | TRUST-WEDGE — CC at workspace-trust dialog, null stdin; `ENDPOINT_RUN_STARTED` printed; ready=false forever | every fresh probe cwd, this session | SILENT |\n 43|| 2 | ACCOUNT-ROOT-MISMATCH — trust seeded in a DIFFERENT config root than the spawn resolves; wedges despite a valid-looking seed | Reavo/Brandon dual-root box (si1 vs b6b probes) | SILENT |\n 44|| 3 | PSYCHE-CRASHLOOP — nested psyche CC dies at trust dialog; wrapper respawned ~3/s, ordinal 5358 in 30 min | F-h; now guarded by adapter v0.13.0 give-up + core C3(b) | SILENT (guarded) |\n 45|| 4 | ADAPTER-UNRESOLVED — no registered adapter hosts the parent binary | seed→listen path, bash parent | LOUD pre-spawn (good shape) |\n 46|| 5 | STALE-CLI-CONTRACT — bringup recipe passes a removed flag; \"unexpected argument\"; spawn never happens | old recipe on 0.22.0+ | LOUD pre-spawn (good shape) |\n*47|| 6 | RELAUNCH-NEVER-BINDS — 2nd `endpoint run` same id after tree-kill; prints but never binds | **0.22.0 only — FIXED on 0.23.0 by F-028 B1/B4** (perri definitive repro 2026-07-03: tree-kill → same-id re-run re-binds fresh session, live PTY, honest info.json; first check was rig-void, --subnet omitted, caught before reporting) | FIXED-NOTE (not in F-027 scope) |\n 48|| — | UAC-740 elevation spawn-fail | NOT observed (subnet-op elevation only) | out of scope |\n 49|\n 50|Seed-1 correction (accepted): the \"backslash-keyed `hasTrustDialogAccepted`\n...\n 65|\n*66|Target invariant (headline): **\"started\" means BOUND, or the failure is LOUD.**\n 67|The silent class (modes 1, 2, 3, 6) must fail with the same loudness as the\n 68|pre-spawn class (modes 4, 5) already does.\n 69|\n...\n 71|\n*72|After the spawn RPC returns in `cmd_endpoint_run --start`, the CLI **awaits\n 73|bind** — the appearance of the `ready` marker (`resolve_ready_file`, the\n*74|`establish_perch` effect) — bounded by a deadline:\n 75|\n*76|- **Success:** `ENDPOINT_RUN_STARTED:{id}` moves to AFTER the bind is observed\n*77|  — its plain meaning finally true. The early machine line `ENDPOINT_RUN:{id}\n 78|  … pid={pid}` stays at spawn-accept (scripts parse it; unchanged).\n*79|- **Deadline exceeded:** emit **`ENDPOINT_SPAWN_FAIL:{id}: reason=<CLASS>:\n*80|  <detail>`** on CLI stderr, exit non-zero, and **stamp the perch record**\n 81|  (`spawn_fail` field via the locked `mutate_info` RMW) so `spt list` and the\n 82|  picker render the honest state. A later successful bind clears the stamp\n*83|  (`establish_perch` already rewrites state; clearing rides there).\n 84|- Classification probes (all CLI-observable; no daemon change needed to\n 85|  detect): child pid alive? broker `has_session` still true? ready marker\n 86|  present? →\n...\n 97|  hatch `--no-wait` restores fire-and-forget and prints\n*98|  `ENDPOINT_RUN_DETACHED:{id}` (explicitly unverified — never `_STARTED`).\n*99|- The attach (non-`--start`) path upgrades the same way: `await_endpoint_session`\n 100|  keeps its session gate, then the same bind-await runs before declaring health.\n 101|\n 102|Failure of the await machinery itself (CLI killed mid-wait) fails open to\n...\n 116|   env vars the adapter declares `[env.<NAME>] direction=\"read\"` are **captured\n*117|   at endpoint creation and stamped into the perch record**; the broker threads\n 118|   the IDENTICAL stamped values to BOTH the pre-spawn command AND the session\n 119|   spawn. Neither consumer reads its own process env for a stamped var — if the\n 120|   seeder read its own env while the spawn read the stamp (or vice versa),\n...\n 132|3. **Bounded + loud:** runtime capped (30 s); non-zero exit or timeout ABORTS\n*133|   the session spawn with `ENDPOINT_SPAWN_FAIL:{id}: reason=PRE_SPAWN_FAIL:\n 134|   <exit/detail>` on CLI stderr (relayed through the spawn RPC error — this\n## F-030-PSYCHE-EPHEMERAL-DESIGN.md\n 17|\n*18|The daemon hosts each live endpoint's Psyche as a **long-lived resident\n 19|process** and enforces that residency:\n 20|\n 21|- `livehost.rs:497 → host_one:518` spawns the psyche\n...\n 48|exit after the seed turn tripped the residency machinery, which tore down the\n*49|endpoint's hosted state — ready marker removed, never re-stamped, every\n 50|force-native gated `leg=cli-gate-not-hosted` **permanently** (field brick,\n 51|operator-hit on hall-bf, contained same-day by v0.13.3).\n 52|\n...\n 77|\n*78|Target invariant (headline): **the Psyche is a per-event turn, not a process.\n 79|Liveness of the psyche = \"turns succeed\", never \"a pid is resident.\"**\n 80|\n 81|### W1 — wire the per-event driver (retire the resident spawn)\n...\n 95|The daemon drives every invocation, so every substitution key is filled from\n*96|**daemon-known context** — the child never self-resolves home/subnet/perch.\n 97|Direction-(a) failures become impossible by construction, not by fixing the\n 98|poll.\n 99|\n...\n 108|The psyche mints and keeps **its own session id**, stored in the nested\n*109|`{id}-psyche` perch record — `{session_id}` in psyche role templates becomes\n 110|**the psyche's sid**, never the parent's (today's fill at livehost.rs:518 is\n 111|the parent's — the custody bug). Rules:\n 112|\n...\n 131|  resident child. REQ-HAZARD-LIVEHOST-NONRESIDENT's **spirit** (a psyche that\n*132|  cannot work must be visible on the perch, and hosting must not churn)\n 133|  transfers to the W1 failure budget; the REQ's traceable entry gets a\n 134|  SUPERSEDED pointer to the new REQ (same pattern as LIVENESS-DECAY →\n 135|  SUPERSEDED in C-1).\n 136|- **The teardown that made direction (b) destructive is deleted**: no psyche\n*137|  state may ever remove the PARENT endpoint's ready marker or hosted state.\n 138|  Psyche trouble stamps psyche fields only. (This is the conformance test's\n 139|  core assertion.)\n 140|- C3(b) guard rework rides here (evidence #6): the failure budget counts\n*141|  **attempts via the psyche perch ledger** (boot records / turn records), not\n 142|  whatever it counted that let 12/min churn run invisibly. Red-first: a\n 143|  12/min synthetic failure loop MUST trip the budget.\n 144|\n...\n 146|\n*147|Even fully ephemeral, child-side verbs acting on nested perches must resolve\n 148|without a `--subnet` the child cannot know. Fix in core, general form:\n 149|**nested `{parent}/{nested}` ids resolve under the parent's home** —\n*150|subnet-less resolution for nested perches (`resolve_perch_path`/ready-verb\n 151|family). Additionally expose a single `{subnet}` base_keys fill **when\n 152|known** (lifecycle.rs:338; absent → LOUD missing-key fail, the `{node}`\n 153|precedent on :344). **There is deliberately NO `{home}` key** (doyle W4 Q1: the\n*154|endpoint home subnet is one concept, and `home:` is already the subnet-name\n 155|qualifier position — a `{home}` template key would only invite meaning-drift).\n 156|Red-first = evidence #3's exact repro: 2-subnet home, nested-id verb, must\n 157|succeed.\n...\n 188|  boundary holds).\n*189|- No relay-unregister fix (the post-clear `owl send → NO_PERCH` casualty) —\n 190|  separate seed, C-2 follow-up (a), field-repro'd 2026-07-04, sequenced with\n 191|  the operator's v0.24.0 bug batch, likely same milestone but its own item.\n 192|- No wake-arc work — closed end-to-end 2026-07-04 (five layers, wakefin3 +\n...\n 212|  bounded per-event turn through resume/turn/relay; no resident psyche process\n*213|  exists between events. Red-first: fire an event on a hosted live endpoint →\n 214|  assert one turn ran (side-effect proof file — transcript-based asserts are\n 215|  structurally blind, 2026-07-04 rig lesson) and no `{id}-psyche` process\n 216|  survives the turn.\n...\n 218|  parent boundary does not rotate it; reseed is loud. Red-first: parent\n*219|  `api boundary clear` → nested perch sid UNCHANGED (today it is the parent's\n 220|  — guard-revert reproduces).\n*221|- `REQ-PSYCHE-NESTED-RESOLUTION` — nested-perch verbs resolve under the parent\n 222|  home without `--subnet` on a multi-subnet node; a single `{subnet}` fill\n 223|  present-or-loud (no `{home}` key). Red-first: evidence #3 repro (2-subnet home rig).\n 224|- `REQ-HAZARD-PSYCHE-RESIDENCY-EXPECTATION` — hazard (paid-for, hall-bf +\n 225|  0.13.2): psyche failure of ANY shape must not remove/alter the parent\n*226|  endpoint's ready/hosted state, and hosting must not churn-respawn.\n*227|  Conformance int = the hall-bf shape: multi-subnet home, live endpoint,\n 228|  failing psyche → parent stays deliverable, no rehost churn, error stamped.\n 229|- `REQ-HAZARD-THRASH-GUARD-BLIND` — hazard (paid-for, evidence #6): the\n 230|  failure budget counts real attempts (ledger-derived); a 12/min failure loop\n...\n 241|  close. Per-wave traceable activation (never full end-state upfront). Gates:\n*242|  isolated worktree per sha + fresh CARGO_TARGET_DIR, nextest, clippy\n 243|  --workspace, docs-drift xtask. `[twohost]` NO — but W4's int needs the\n 244|  **single-node multi-subnet rig** (the hall-bf shape: 2 subnets, one home).\n 245|- **Adapter (perri):** `psyche-ephemeral-parked` branch rebased once core API\n...\n 259|3. `{subnet}` fill naming — RESOLVED (doyle W4 Q1): a single `{subnet}` key,\n*260|   no `{home}` key (the endpoint home subnet is one concept; `home:` is already\n 261|   the subnet-name qualifier position). Confirmed against CONTEXT.md glossary.\n## F-035-CONFLICT.md\n 16|\n*17|- **Setup:** spools `spool_message_deferred_at(perch, \"peer\", …)` — which is\n 18|  `insert_message(…, WINDOW_ACTIVE_ONLY, …)` (spool.rs:363). So the row is\n 19|  **`active_only`**.\n 20|- **Trigger:** `spt api state idle <id>` — the real ACTIVE→IDLE edge.\n...\n 34|\n*35|For an **spt-hosted** endpoint (broker owns the PTY, `controllable`, relay-less), the\n 36|idle-edge/parked drain is the **ONLY** carrier for an `active_only` row. Traced every path:\n 37|\n 38|| carrier | `active_only` to spt-hosted? |\n...","truncated":true,"truncatedBy":"bytes","totalLines":858,"totalBytes":99682,"outputLines":467,"outputBytes":51115,"lastLinePartial":false,"firstLineExceedsLimit":false},"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}},"truncation":{"direction":"head","truncatedBy":"bytes","totalLines":858,"totalBytes":99682,"outputLines":467,"outputBytes":51115,"shownRange":{"start":1,"end":467},"nextOffset":468}}},"isError":false,"timestamp":1784197376878}}
{"type":"message","id":"e0d2baa3","parentId":"a67770a3","timestamp":"2026-07-16T10:23:05.384Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Mapping crates directory and searching elevation endpoint**"},{"type":"toolCall","id":"call_vDMxAoudtg7eP2UaGZiyo2Ig|fc_017bff223290b7ba016a58b1095d0c819aa82a8f6e0f73f868","name":"glob","arguments":{"i":"Mapping Rust implementation","path":"crates/**/*.rs","hidden":false,"gitignore":true,"limit":1000}},{"type":"toolCall","id":"call_xxWmGOGi2vrcM7nWN8RECmd4|fc_017bff223290b7ba016a58b1095d18819ab4508996460c3405","name":"grep","arguments":{"i":"Locating elevated endpoint fields","pattern":"elevated_endpoint|elevated endpoint|is_elevated|elevation|elevated","path":"crates; docs/KNOWN-HAZARDS.md; docs/adr; CONTEXT.md","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_X8zFRiAL7B2eJiCkOymcIBl5|fc_017bff223290b7ba016a58b1095d24819a86a50c9931500dcd","name":"grep","arguments":{"i":"Locating registry bind flow","pattern":"establish_perch|ready_file|bind_perch|api bind|Listener|listen|register.*endpoint|Instance \\{|struct Instance","path":"crates","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_DqAnivMNVIDC6L2ES0YWI7QA|fc_017bff223290b7ba016a58b1095d28819a8b29a3a8ce0b80da","name":"grep","arguments":{"i":"Locating spool routing flow","pattern":"spool_message|drain_|claim_|insert_message|wansend|route_|resolve_target|deliver\\(","path":"crates","selector":null,"case":true,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":44561,"output":303,"cacheRead":13824,"cacheWrite":0,"totalTokens":58688,"reasoningTokens":27,"cost":{"input":0.22280500000000003,"output":0.00909,"cacheRead":0.006912,"cacheWrite":0,"total":0.23880700000000002}},"stopReason":"toolUse","timestamp":1784197376944,"responseId":"resp_017bff223290b7ba016a58b101419c819a91de7cb2b5b44843","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b103e220819aaa6618ff1d14261f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLEJUyUrrOAfPb0CXUQJr6UPLg4EDPtNSQF-33TkJWOmGJYT7wvfJZ9GXEgx59Fj56WhMSw1t3OUWGvFs9-HqHMdMaDtCBXgJ0NMtnzyKT1O-bSndFK6kquicTHzdAHkiBG6Nl8_3q8FONV1UMtmb0AQQaLneqv1b9MefhP0bHFavBw9Wyq2FKg62I8zUBjc0sMAvuVh2eo2lY24-zHinnc5bRsr-Zn3kBDVa46QystTr87X4DaZnhJhTswbmhFRY2wm8DdI4z7CEEnzdNJIXONG-JFL6_TayOKrKl6jnkue16Lq46fSMAuy-27nbXL5sjngWsP85w0E2mJZViO4ORpQQHrgCGw58bsb6QnEsVSgiIWu3V5RfliD_IMWFpaWyv-5hxJOo3ErK1Ld20cFKY3INx20JMJR2Kw10BUF7fumMjTxW0gNP_uhIFq3MRUdZy9Qe3o32SeN7uIjK4NNjaRfGd9vALwJcdBJL6WdW7qC6I9E3jpNVvkX70h_8sAh0HH0BkavJjlsV8w2jgXLerIZq-s5BuULF80j9qibSQ_hAaAwv47KSKfUGJt8ZYBqZ_946GLXPPqdAISUvzcvyxa_YwzbeMKs23ozGmkyulO1H0vkJa_PdTu2t4iSmhG3fxWuWv_a8lvWXrX0rxr_nVBQPRgW72-Ld3DUgxUuquBUf8esQJ3OV2cTGuhcoI_IR8BymWxiD6sx6LBLmx3OLb2k_HXr0aPTyl7Gc4WCYXNNA94cIPRYkyT9wT8Sdl4CNSzQIdysBSoSDXvA6qyx4SQEOVrK-eztzwbKv1uxnim87Nu5fN4bKBjFCUPE-RyqVSXpGDjDrDuNj1TT8vmX45RrS87zcqeBAQ-KxD-k55G95QZHzE9W3ofrGOn8FF0mRJgmu53-zJLgDG1RpMTGXlMf83lP3KR29U2NlYw6LnCZVheM7QC95jB_-IVUQ546ITUKaYYxCv-Wj2H2wtdrXMqWzKnPUoAhKploKqXXzALloE6xsINrrcCcVruWiaZkmj3pAUtLkRLDd_YQG_RHPs5RsnrG9hoxrgB_ZEswlzV3T3a5Pq92b6mWbcy1-7b0ScceU31JZqS8XVseC-RcY_UuKQ==","summary":[{"type":"summary_text","text":"**Mapping crates directory and searching elevation endpoint**"}]},{"id":"fc_017bff223290b7ba016a58b1095d0c819aa82a8f6e0f73f868","type":"function_call","status":"completed","arguments":"{\"i\":\"Mapping Rust implementation\",\"path\":\"crates/**/*.rs\",\"hidden\":false,\"gitignore\":true,\"limit\":1000}","call_id":"call_vDMxAoudtg7eP2UaGZiyo2Ig","name":"glob"},{"id":"fc_017bff223290b7ba016a58b1095d18819ab4508996460c3405","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating elevated endpoint fields\",\"pattern\":\"elevated_endpoint|elevated endpoint|is_elevated|elevation|elevated\",\"path\":\"crates; docs/KNOWN-HAZARDS.md; docs/adr; CONTEXT.md\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_xxWmGOGi2vrcM7nWN8RECmd4","name":"grep"},{"id":"fc_017bff223290b7ba016a58b1095d24819a86a50c9931500dcd","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating registry bind flow\",\"pattern\":\"establish_perch|ready_file|bind_perch|api bind|Listener|listen|register.*endpoint|Instance \\\\{|struct Instance\",\"path\":\"crates\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_X8zFRiAL7B2eJiCkOymcIBl5","name":"grep"},{"id":"fc_017bff223290b7ba016a58b1095d28819a8b29a3a8ce0b80da","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating spool routing flow\",\"pattern\":\"spool_message|drain_|claim_|insert_message|wansend|route_|resolve_target|deliver\\\\(\",\"path\":\"crates\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_DqAnivMNVIDC6L2ES0YWI7QA","name":"grep"}]},"duration":8439.19810000062,"ttft":2912.4917000001296,"contextSnapshot":{"promptTokens":58385,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_vDMxAoudtg7eP2UaGZiyo2Ig|fc_017bff223290b7ba016a58b1095d0c819aa82a8f6e0f73f868","toolName":"glob","startedAt":"2026-07-16T10:23:05.384Z","args":{"path":"crates/**/*.rs"},"intent":"Mapping Rust implementation"},"id":"ccd224bf","parentId":"e0d2baa3","timestamp":"2026-07-16T10:23:05.384Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xxWmGOGi2vrcM7nWN8RECmd4|fc_017bff223290b7ba016a58b1095d18819ab4508996460c3405","toolName":"grep","startedAt":"2026-07-16T10:23:05.384Z","args":{"path":"crates; docs/KNOWN-HAZARDS.md; docs/adr; CONTEXT.md"},"intent":"Locating elevated endpoint fields"},"id":"7fe34b15","parentId":"ccd224bf","timestamp":"2026-07-16T10:23:05.384Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_X8zFRiAL7B2eJiCkOymcIBl5|fc_017bff223290b7ba016a58b1095d24819a86a50c9931500dcd","toolName":"grep","startedAt":"2026-07-16T10:23:05.384Z","args":{"path":"crates"},"intent":"Locating registry bind flow"},"id":"24003654","parentId":"7fe34b15","timestamp":"2026-07-16T10:23:05.384Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_DqAnivMNVIDC6L2ES0YWI7QA|fc_017bff223290b7ba016a58b1095d28819a8b29a3a8ce0b80da","toolName":"grep","startedAt":"2026-07-16T10:23:05.384Z","args":{"path":"crates"},"intent":"Locating spool routing flow"},"id":"b6626c68","parentId":"24003654","timestamp":"2026-07-16T10:23:05.384Z"}
{"type":"message","id":"02d2314c","parentId":"b6626c68","timestamp":"2026-07-16T10:23:05.410Z","message":{"role":"toolResult","toolCallId":"call_vDMxAoudtg7eP2UaGZiyo2Ig|fc_017bff223290b7ba016a58b1095d0c819aa82a8f6e0f73f868","toolName":"glob","content":[{"type":"text","text":"# crates/\n## spt/\n### tests/\nprojindex_reader_e2e.rs\ndummy_harness_e2e.rs\nprojindex_writer_e2e.rs\njson_emit.rs\nwhoami_identity_e2e.rs\ndaemon_refresh_e2e.rs\ncomposite_e2e.rs\ndocs_bundle_e2e.rs\nrelease_verify_e2e.rs\nbootstrap_verb_e2e.rs\ntrial_drain_drive_e2e.rs\nresume_no_control_steal_e2e.rs\nsend_stamp_agent_id_e2e.rs\nendpoint_autostart_e2e.rs\nlist_json_liveness_parity_e2e.rs\nlisten_seed_retry_e2e.rs\nactive_only_never_relay_e2e.rs\nn1_pairing.rs\nattach_wedge_e2e.rs\nworker_visibility_e2e.rs\nworker_lifecycle_e2e.rs\ntwohost_cli.rs\ntranslate_proof.rs\npsyche_download_e2e.rs\nnotify_shell_e2e.rs\nmulti_subnet_bringup_e2e.rs\nlive_adapt_translation_swap_e2e.rs\nbrain_survive.rs\n#### fixtures/\ngit_fixture.rs\ngh_fixture.rs\n### src/\ncli.rs\nroster.rs\nrc.rs\nwansend.rs\n#### picker/\ndata.rs\nmod.rs\nview.rs\nmodel.rs\n#### api/\nreporting.rs\nstartup.rs\nauth.rs\nmod.rs\nworker.rs\ndelivery.rs\n## spt-store/\n### src/\nprojindex.rs\nprojderive.rs\nprojinval.rs\nlib.rs\nbranchstore.rs\ncontextstore.rs\ngitrun.rs\npsyche_custody.rs\nspool.rs\npeeraddrs.rs\nworker_reap.rs\nworker_seq.rs\ninfo.rs\nsessions.rs\nxfer.rs\nvisibility.rs\ntimefmt.rs\nsyncmerge.rs\nsubnet.rs\nshellinfo.rs\nseed.rs\nrotation.rs\nroster.rs\nrename.rs\nregistry.rs\nrecent_home.rs\nproject.rs\nproc.rs\nperch.rs\nnotif.rs\nnodeid.rs\nliveness.rs\nhostlabel.rs\nhome.rs\nhistory.rs\ngrants.rs\nepoch.rs\ndb.rs\ncontextmark.rs\nattachment.rs\natomic.rs\naccess.rs\n### tests/\ncarrier_claim_int.rs\n## spt-daemon/\n### src/\nsync.rs\nprojwriter.rs\nlib.rs\nbrainproc.rs\napplyhost.rs\ndocshost.rs\ndaemon.rs\nconfig.rs\nupdate.rs\nrelcache.rs\nrelease.rs\nnotif.rs\nrelay.rs\npropagate.rs\nmsg.rs\nnethost.rs\nlivehost.rs\ninject.rs\nconn.rs\nbroker.rs\nbrain.rs\nautostart.rs\nstderrlog.rs\nlifecycle.rs\ndigest.rs\npairhost.rs\nxfer.rs\nshellwake.rs\nshellchan.rs\nseedmap.rs\nresting.rs\nresthost.rs\nregistryhost.rs\nlinkhost.rs\neffect.rs\nattach.rs\nadapter_update.rs\n#### pump/\nmod.rs\n#### bin/\nxlate_choreo_fixture.rs\n### tests/\ndocs_server_e2e.rs\npropagate.rs\npumpdeadline.rs\npump.rs\nfalse_promote.rs\nnet_worker_starve.rs\nbrain_resume_conn_deadlock.rs\nconn_blackhole_lifecycle.rs\nbrain_decouple.rs\ninject_control_wedge.rs\ntwo_origin_spanning.rs\nwake_single_flight.rs\nendpoint_survival.rs\nxfer.rs\nwanmsg.rs\ntwohost.rs\nreplicate.rs\nsync.rs\nnotifsync.rs\nnetstream.rs\nnetbroker.rs\ninput_ack_deadlock.rs\nidempotent.rs\ndispatch.rs\ndaemon_e2e.rs\nbroker.rs\nattach.rs\naccess.rs\n## spt-msg/src/\nready.rs\ndeliver.rs\n## xtask/src/\nmain.rs\n## spt-runtime/src/\nmanifest.rs\nruntime.rs\nresolve.rs\nlib.rs\nregistry.rs\nprofile.rs\n## spt-live/src/\npulse.rs\ningest.rs\necho.rs\n## spt-net/src/\nnet.rs\n### net/\nregistry.rs\nxfer.rs\nwanmsg.rs\nupdate.rs\nsync.rs\nshelllink.rs\nserveprobe.rs\nrest.rs\n#### pairing/\nntp.rs\n## spt-term/\n### src/\nscreen.rs\nwinprog.rs\nsurface.rs\nstream.rs\nreader.rs\npty.rs\nprojection.rs\nlib.rs\ndigest.rs\n### tests/\nwinspawn.rs\nsurface.rs\nstream.rs\ninject.rs\ndsr.rs\n## spt-proto/src/\nversion.rs\npayload.rs\nlib.rs\nidentity.rs\nid.rs\nevent.rs\nenvelope.rs\nendpoint.rs\nchunk.rs\naddr.rs\n\n[200 results limit reached. Use limit=400 for more]"}],"details":{"scopePath":"crates","fileCount":200,"files":["crates/spt/tests/projindex_reader_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs","crates/spt/src/picker/data.rs","crates/spt/src/cli.rs","crates/spt/src/api/reporting.rs","crates/spt-store/src/projindex.rs","crates/spt-store/src/projderive.rs","crates/spt-daemon/src/sync.rs","crates/spt-daemon/src/projwriter.rs","crates/spt/tests/projindex_writer_e2e.rs","crates/spt/src/api/startup.rs","crates/spt-store/src/projinval.rs","crates/spt-store/src/lib.rs","crates/spt-store/src/branchstore.rs","crates/spt-store/src/contextstore.rs","crates/spt-msg/src/ready.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/brainproc.rs","crates/spt/tests/json_emit.rs","crates/spt/tests/whoami_identity_e2e.rs","crates/spt/src/roster.rs","crates/spt/tests/fixtures/git_fixture.rs","crates/spt-store/src/gitrun.rs","crates/spt/tests/daemon_refresh_e2e.rs","crates/spt/tests/fixtures/gh_fixture.rs","crates/spt/tests/composite_e2e.rs","crates/spt-daemon/src/applyhost.rs","crates/xtask/src/main.rs","crates/spt-daemon/tests/docs_server_e2e.rs","crates/spt-daemon/src/docshost.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/config.rs","crates/spt/tests/docs_bundle_e2e.rs","crates/spt-runtime/src/manifest.rs","crates/spt-daemon/tests/propagate.rs","crates/spt-daemon/src/update.rs","crates/spt-daemon/src/relcache.rs","crates/spt-daemon/src/release.rs","crates/spt/tests/release_verify_e2e.rs","crates/spt/tests/bootstrap_verb_e2e.rs","crates/spt-daemon/src/notif.rs","crates/spt/tests/trial_drain_drive_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/send_stamp_agent_id_e2e.rs","crates/spt/tests/endpoint_autostart_e2e.rs","crates/spt/tests/list_json_liveness_parity_e2e.rs","crates/spt/tests/listen_seed_retry_e2e.rs","crates/spt/tests/active_only_never_relay_e2e.rs","crates/spt/src/picker/mod.rs","crates/spt/src/api/auth.rs","crates/spt/src/api/mod.rs","crates/spt-store/tests/carrier_claim_int.rs","crates/spt-store/src/psyche_custody.rs","crates/spt-store/src/spool.rs","crates/spt-store/src/peeraddrs.rs","crates/spt-runtime/src/runtime.rs","crates/spt-runtime/src/resolve.rs","crates/spt-runtime/src/lib.rs","crates/spt-live/src/pulse.rs","crates/spt-msg/src/deliver.rs","crates/spt-live/src/ingest.rs","crates/spt-daemon/tests/pumpdeadline.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/false_promote.rs","crates/spt-daemon/tests/net_worker_starve.rs","crates/spt-daemon/tests/brain_resume_conn_deadlock.rs","crates/spt-daemon/tests/conn_blackhole_lifecycle.rs","crates/spt-daemon/tests/brain_decouple.rs","crates/spt-daemon/src/relay.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/propagate.rs","crates/spt-daemon/src/msg.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/inject.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/autostart.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt/tests/n1_pairing.rs","crates/spt/tests/attach_wedge_e2e.rs","crates/spt/src/rc.rs","crates/spt/src/api/worker.rs","crates/spt/src/api/delivery.rs","crates/spt-live/src/echo.rs","crates/spt-daemon/tests/two_origin_spanning.rs","crates/spt-daemon/tests/wake_single_flight.rs","crates/spt-daemon/tests/endpoint_survival.rs","crates/spt-daemon/src/stderrlog.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/digest.rs","crates/spt-daemon/src/bin/xlate_choreo_fixture.rs","crates/spt-net/src/net/pairing/ntp.rs","crates/spt-daemon/src/pairhost.rs","crates/spt/tests/worker_visibility_e2e.rs","crates/spt/tests/worker_lifecycle_e2e.rs","crates/spt-store/src/worker_reap.rs","crates/spt-store/src/worker_seq.rs","crates/spt-store/src/info.rs","crates/spt-runtime/src/registry.rs","crates/spt/tests/twohost_cli.rs","crates/spt/tests/translate_proof.rs","crates/spt/tests/psyche_download_e2e.rs","crates/spt/tests/notify_shell_e2e.rs","crates/spt/tests/multi_subnet_bringup_e2e.rs","crates/spt/tests/live_adapt_translation_swap_e2e.rs","crates/spt/tests/brain_survive.rs","crates/spt/src/wansend.rs","crates/spt/src/picker/view.rs","crates/spt/src/picker/model.rs","crates/spt-term/src/screen.rs","crates/spt-store/src/sessions.rs","crates/spt-net/src/net/registry.rs","crates/spt-daemon/tests/xfer.rs","crates/spt-daemon/tests/wanmsg.rs","crates/spt-daemon/tests/twohost.rs","crates/spt-daemon/tests/replicate.rs","crates/spt-daemon/tests/sync.rs","crates/spt-daemon/tests/notifsync.rs","crates/spt-daemon/tests/netstream.rs","crates/spt-daemon/tests/netbroker.rs","crates/spt-daemon/tests/input_ack_deadlock.rs","crates/spt-daemon/tests/idempotent.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/broker.rs","crates/spt-daemon/tests/attach.rs","crates/spt-daemon/tests/access.rs","crates/spt-daemon/src/xfer.rs","crates/spt-daemon/src/shellwake.rs","crates/spt-daemon/src/shellchan.rs","crates/spt-daemon/src/seedmap.rs","crates/spt-daemon/src/resting.rs","crates/spt-daemon/src/resthost.rs","crates/spt-daemon/src/registryhost.rs","crates/spt-daemon/src/linkhost.rs","crates/spt-daemon/src/effect.rs","crates/spt-daemon/src/attach.rs","crates/spt-daemon/src/adapter_update.rs","crates/spt-term/tests/winspawn.rs","crates/spt-term/tests/surface.rs","crates/spt-term/tests/stream.rs","crates/spt-term/tests/inject.rs","crates/spt-term/tests/dsr.rs","crates/spt-term/src/winprog.rs","crates/spt-term/src/surface.rs","crates/spt-term/src/stream.rs","crates/spt-term/src/reader.rs","crates/spt-term/src/pty.rs","crates/spt-term/src/projection.rs","crates/spt-term/src/lib.rs","crates/spt-term/src/digest.rs","crates/spt-store/src/xfer.rs","crates/spt-store/src/visibility.rs","crates/spt-store/src/timefmt.rs","crates/spt-store/src/syncmerge.rs","crates/spt-store/src/subnet.rs","crates/spt-store/src/shellinfo.rs","crates/spt-store/src/seed.rs","crates/spt-store/src/rotation.rs","crates/spt-store/src/roster.rs","crates/spt-store/src/rename.rs","crates/spt-store/src/registry.rs","crates/spt-store/src/recent_home.rs","crates/spt-store/src/project.rs","crates/spt-store/src/proc.rs","crates/spt-store/src/perch.rs","crates/spt-store/src/notif.rs","crates/spt-store/src/nodeid.rs","crates/spt-store/src/liveness.rs","crates/spt-store/src/hostlabel.rs","crates/spt-store/src/home.rs","crates/spt-store/src/history.rs","crates/spt-store/src/grants.rs","crates/spt-store/src/epoch.rs","crates/spt-store/src/db.rs","crates/spt-store/src/contextmark.rs","crates/spt-store/src/attachment.rs","crates/spt-store/src/atomic.rs","crates/spt-store/src/access.rs","crates/spt-runtime/src/profile.rs","crates/spt-proto/src/version.rs","crates/spt-proto/src/payload.rs","crates/spt-proto/src/lib.rs","crates/spt-proto/src/identity.rs","crates/spt-proto/src/id.rs","crates/spt-proto/src/event.rs","crates/spt-proto/src/envelope.rs","crates/spt-proto/src/endpoint.rs","crates/spt-proto/src/chunk.rs","crates/spt-proto/src/addr.rs","crates/spt-net/src/net.rs","crates/spt-net/src/net/xfer.rs","crates/spt-net/src/net/wanmsg.rs","crates/spt-net/src/net/update.rs","crates/spt-net/src/net/sync.rs","crates/spt-net/src/net/shelllink.rs","crates/spt-net/src/net/serveprobe.rs","crates/spt-net/src/net/rest.rs"],"truncated":true,"resultLimitReached":200,"cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","meta":{"limits":{"resultLimit":{"reached":200,"suggestion":400}}}},"isError":false,"timestamp":1784197385410}}
{"type":"message","id":"daf635b0","parentId":"02d2314c","timestamp":"2026-07-16T10:23:05.419Z","message":{"role":"toolResult","toolCallId":"call_DqAnivMNVIDC6L2ES0YWI7QA|fc_017bff223290b7ba016a58b1095d28819a8b29a3a8ce0b80da","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/\n\n### src/\n#### brainproc.rs\n 1462|        // after N polls (the ready-seen latch holds across the undrained polls).\n*1463|        drain_countdown: AtomicU32,\n 1464|    }\n 1465|\n 1466|    impl TestTrialEnv {\n...\n 1481|                staged_hash: Mutex::new(None),\n*1482|                drain_countdown: AtomicU32::new(0),\n 1483|            }\n 1484|        }\n 1485|\n...\n 1496|        fn drains_after(self, polls: u32) -> Self {\n*1497|            self.drain_countdown.store(polls, Ordering::Relaxed);\n 1498|            self\n 1499|        }\n 1500|\n...\n 1546|            // sequentially), so a plain load/store is race-free here.\n*1547|            let left = self.drain_countdown.load(Ordering::Relaxed);\n 1548|            if left == 0 {\n 1549|                return true;\n 1550|            }\n*1551|            self.drain_countdown.store(left - 1, Ordering::Relaxed);\n 1552|            false\n 1553|        }\n 1554|    }\n#### broker.rs\n 2452|        spt_store::perch::resolve_perch_path(endpoint, spt_store::perch::ParentHint::Infer);\n*2453|    if let Err(e) = spt_store::spool::spool_message_at(&perch_path, \"\", envelope) {\n 2454|        eprintln!(\n 2455|            \"TRANSLATION_FAULT_RESPOOL:{endpoint}: failed to re-spool ({e}) — message dropped\"\n 2456|        );\n...\n 4632|    /// forever (Windows named pipes reject native read deadlines — see\n*4633|    /// `drain_output_seqs`'s note). Used to PROVE the absence of a trailing marker on\n 4634|    /// a normal teardown: a normal `viewer_writer` exit drops its send-half clone, the\n 4635|    /// client read then hits EOF, and `read_frame` returns Err → `None`.\n 4636|    fn try_read_frame(client: Stream, wait: Duration) -> Option<Envelope> {\n...\n 5480|    /// (no per-read deadline: Windows named pipes reject I/O timeouts).\n*5481|    fn drain_output_seqs(client: &mut Stream, n: usize) -> Vec<u64> {\n 5482|        let mut seqs = Vec::with_capacity(n);\n 5483|        while seqs.len() < n {\n 5484|            let env = read_frame(client).expect(\"output frame on the wire\");\n...\n 5562|\n*5563|        let seqs = drain_output_seqs(&mut client, 1);\n 5564|        assert_eq!(\n 5565|            seqs,\n 5566|            vec![2],\n#### inject.rs\n 107|// [impl->REQ-HAZARD-DELIVERY-STARVATION]\n*108|pub fn drain_idle_spool(id: &str, owlery: &Path) -> usize {\n 109|    if !is_spt_hosted_no_relay(id, owlery) {\n 110|        return 0;\n 111|    }\n...\n 123|    let claimed =\n*124|        match spt_store::spool::claim_idle_edge_audited_at(&perch_path, include_deferred, &audit) {\n 125|            Ok(rows) if !rows.is_empty() => rows,\n 126|            _ => return 0,\n 127|        };\n...\n 270|            // Pending row but NOT idle → skip (the broker's active-gate owns *when*).\n*271|            spt_store::spool::spool_message_at(&perch_path, \"bob\", \"hi\").unwrap();\n 272|            assert!(\n 273|                !has_parked_idle_spool(id, &owlery),\n 274|                \"pending but not idle → the belt skips it\"\n#### lib.rs\n 250|    apply_event, arm_transition_echo, daemon_rest_event, effective_auto_suspend, fire_wake_effects,\n*251|    read_rest, request_freshness_pull, route_rest_event, take_freshness_pull, transition,\n 252|    write_rest, EdgeReport, RestEvent, RestRecord, RestRoute, RestState,\n 253|    NOT_A_HOSTED_PERCH_MARKER, PULL_MARKER_FILE,\n 254|};\n#### lifecycle.rs\n 610|        // The project tier routes under the cwd-derived project_id, but an owlery-internal\n*611|        // cwd (psyche-host) routes NO project (empty id → route_slices skips commit_project);\n 612|        // the live/agent slice still commits. A relative dir with no recorded cwd is\n 613|        // SKIPPED loud-once, never guessed.\n 614|        // [impl->REQ-STORE-CONTEXT-BRANCH-FILL]\n#### livehost.rs\n 265|        if crate::inject::has_parked_idle_spool(&id, owlery) {\n*266|            let _ = crate::inject::drain_idle_spool(&id, owlery);\n 267|        }\n 268|    }\n 269|}\n#### nethost.rs\n 1444|    // [impl->REQ-SHELL-4]\n*1445|    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n 1446|        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n 1447|        let drained = entry.log.lock().unwrap().drain();\n 1448|        // The drain freed ring room — wake a pump that was backpressure-parked.\n...\n 1825|        for _ in 0..2000 {\n*1826|            if let Some((mut bytes, fin)) = host.drain_stream(shell) {\n 1827|                got.append(&mut bytes);\n 1828|                if fin {\n 1829|                    finished = true;\n#### notif.rs\n 613|            );\n*614|            let rows = spool::drain_all_at(&ling).unwrap();\n 615|            // Structural carriage (ADR-0020): from=\"issuer\" + body = the typed\n 616|            // <EVENT notify> envelope verbatim — NO legacy reply-to wrapper, NO\n 617|            // double-prefix. The delivery edge passes the typed body through.\n...\n 678|            );\n*679|            assert_eq!(spool::drain_all_at(&doyle).unwrap().len(), 1);\n 680|        });\n 681|    }\n 682|\n...\n 733|            assert!(\n*734|                spool::drain_all_at(&ling).unwrap().is_empty(),\n 735|                \"nothing delivered locally\"\n 736|            );\n 737|            let stored = s.get(&row.notif_id).unwrap().unwrap();\n...\n 750|            );\n*751|            assert_eq!(spool::drain_all_at(&ling).unwrap().len(), 1);\n 752|        });\n 753|    }\n 754|\n...\n 831|            );\n*832|            assert_eq!(spool::drain_all_at(&doyle).unwrap().len(), 1);\n 833|\n 834|            // The same boundary again: doyle has now seen it — suppressed,\n 835|            // nothing re-delivered (dismissal is the only further ack).\n...\n 850|            );\n*851|            assert!(spool::drain_all_at(&doyle).unwrap().is_empty());\n 852|        });\n 853|    }\n 854|\n...\n 978|            );\n*979|            assert_eq!(spool::drain_all_at(&ling).unwrap().len(), 1);\n 980|        });\n 981|    }\n 982|\n...\n 1011|            );\n*1012|            assert_eq!(spool::drain_all_at(&ling).unwrap().len(), 1, \"first-fired\");\n 1013|\n 1014|            // Loud = resurfaces: a boundary on a fresh endpoint past the window\n 1015|            // re-delivers the undismissed rollback row (never a one-shot).\n...\n 1220|            );\n*1221|            assert!(spool::drain_all_at(&ghost).unwrap().is_empty());\n 1222|        });\n 1223|    }\n 1224|}\n#### presence.rs\n 342|    // [unit->REQ-PRES-1] snapshot loading degrades on junk exactly like the\n*343|    // wansend reader: missing dir, non-json, junk json all read as absent.\n 344|    #[test]\n 345|    fn snapshot_loading_degrades_on_junk() {\n 346|        let dir = tempfile::tempdir().unwrap();\n#### projwriter.rs\n 638|        // unchanged store exits without a scan (the cold/warm start gate).\n*639|        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n 640|        let boot = Coalesced { global: true, ..boot };\n 641|        if let Err(e) = engine.reconcile(&boot, false) {\n 642|            engine.record_failure(e);\n...\n 672|            }\n*673|            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n 674|            if due_periodic {\n 675|                since_periodic = Duration::ZERO;\n 676|            }\n#### psyrelay.rs\n 302|            );\n*303|            let rows = spool::drain_all_at(&alice).unwrap();\n 304|            assert_eq!(\n 305|                rows,\n 306|                vec![spool::DrainedMsg {\n...\n 334|            // the structural `from` is the re-stamped psyche id, never the spoof.\n*335|            let rows = spool::drain_all_at(&alice).unwrap();\n 336|            assert_eq!(\n 337|                rows,\n 338|                vec![spool::DrainedMsg {\n...\n 345|            // The Psyche-addressed target received nothing.\n*346|            assert!(spool::drain_all_at(&victim).unwrap().is_empty());\n 347|        });\n 348|    }\n 349|\n...\n 371|            );\n*372|            let rows = spool::drain_all_at(&doyle).unwrap();\n 373|            assert_eq!(rows.len(), 1);\n 374|            // Structural carriage: from = re-stamped psyche id, body = the typed\n 375|            // notify <EVENT> verbatim (no legacy reply-to wrapper, no double-prefix).\n...\n 406|            assert!(\n*407|                spool::drain_all_at(&doyle).unwrap().is_empty(),\n 408|                \"nothing delivered now\"\n 409|            );\n 410|            let row = h.store.get(\"cafe:1\").unwrap().unwrap();\n...\n 424|            assert_eq!(got, vec![RelayOutcome::DroppedNoReplyTarget]);\n*425|            assert!(spool::drain_all_at(&doyle).unwrap().is_empty());\n 426|        });\n 427|    }\n 428|\n...\n 455|            assert!(matches!(err, TurnError::EmptyOutput), \"got {err}\");\n*456|            assert!(spool::drain_all_at(&alice).unwrap().is_empty());\n 457|        });\n 458|    }\n 459|\n...\n 466|            assert!(got.is_empty());\n*467|            assert!(spool::drain_all_at(&alice).unwrap().is_empty());\n 468|        });\n 469|    }\n 470|\n...\n 496|            assert_eq!(over, vec![RelayOutcome::DroppedOversize]);\n*497|            assert!(spool::drain_all_at(&alice).unwrap().is_empty());\n 498|        });\n 499|    }\n 500|}\n#### relay.rs\n 62|    /// killed one left behind. Returns the number of messages forwarded.\n*63|    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {\n 64|        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg — stamp who drained the row.\n 65|        let audit =\n 66|            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\n*67|        let msgs = spool::drain_non_deferred_audited_at(&self.perch_path, &audit).unwrap_or_default();\n 68|        let n = msgs.len();\n 69|        for m in &msgs {\n 70|            sink(&m.from, &m.body);\n...\n 80|        }\n*81|        self.drain_backlog(&mut sink);\n 82|    }\n 83|\n 84|    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as\n...\n 89|        // Drain whatever a predecessor relay left behind before streaming live.\n*90|        self.drain_backlog(&mut sink);\n 91|        while !stop.load(Ordering::Relaxed) {\n 92|            self.pump_once(&mut sink);\n 93|        }\n...\n 112|            let perch = perch_for(\"doyle\");\n*113|            spool::spool_message_at(&perch, \"alice\", \"m1\").unwrap();\n*114|            spool::spool_message_at(&perch, \"alice\", \"m2\").unwrap();\n 115|\n 116|            // Relay A binds then is \"killed\" (dropped) WITHOUT pumping. The two\n 117|            // messages remain durable in the spool — the relay held no state.\n...\n 125|            let mut got = Vec::new();\n*126|            let n = b.drain_backlog(|from, body| got.push((from.to_string(), body.to_string())));\n 127|            assert_eq!(n, 2);\n 128|            assert!(got.iter().all(|(f, _)| f == \"alice\"));\n 129|            assert!(got.iter().any(|(_, b)| b.contains(\"m1\")));\n...\n 135|    // [unit->REQ-SEND-WINDOW-DRAIN-HONOR] F-035 GUARD 3 (relay backlog): the relay's\n*136|    // backlog drain (`drain_backlog` → `drain_non_deferred`, `WHERE deferred = 0`)\n 137|    // NEVER forwards an `active_only` row. `active_only` is poll-only — it must never\n 138|    // reach a relay-bearing live agent's idle relay (the field symptom this locks:\n 139|    // an active_only msg surfacing on lia's idle relay). A `default` row on the same\n...\n 146|            // default → relay-eligible (idle/wake window); active_only → poll-only.\n*147|            spool::spool_message_at(&perch, \"peer\", \"default-ok\").unwrap();\n*148|            spool::spool_message_deferred_at(&perch, \"peer\", \"active-only-noise\").unwrap();\n 149|\n 150|            let relay = Relay::bind(\"lia\", &perch).unwrap();\n 151|            let mut got = Vec::new();\n*152|            relay.drain_backlog(|from, body| got.push((from.to_string(), body.to_string())));\n 153|\n 154|            assert_eq!(got.len(), 1, \"only the default row rides the relay backlog\");\n 155|            assert_eq!(got[0].1, \"default-ok\");\n...\n 175|            let mut got = Vec::new();\n*176|            spool::spool_message_at(&perch, \"alice\", \"hi\").unwrap();\n 177|            relay.run(&stop, |_from, body| got.push(body.to_string()));\n 178|            assert_eq!(got.len(), 1, \"the pre-stream backlog drain still fires\");\n 179|        });\n#### resting.rs\n 471|/// - `spt::api::delivery::poll_drain` (`api poll --include-deferred`,\n*472|///   `drain_all_at`) — **gated**.\n*473|/// - `spt::api::worker::cmd_worker_poll` (`drain_all_at`) — **gated**.\n 474|/// - `spt_daemon::shellchan::deliver_stdin_pending_in` (`peek_all_at` on a\n 475|///   SHELL perch) — not gated: no writer ever defers onto a shell perch\n 476|///   (`spool_shell_frame` spools non-deferred only), and a suspended owner's\n 477|///   online shells are closed by the edge cascade anyway.\n*478|/// - `spt_msg::ring` (`drain_one_at` on the ephemeral reply perch) — not\n 479|///   gated: that perch is recordless by construction (created and deleted\n 480|///   inside one ring call), which IS the legacy fallback; spt-msg also sits\n 481|///   below this crate in the dependency order.\n*482|/// - The event stream (`drain_non_deferred_at`) was never a deferred surface\n 483|///   (KH 1.4) and stays unfiltered-of-this-gate: non-deferred flow while\n 484|///   resting is untouched.\n 485|// [impl->REQ-INST-6]\n...\n 538|// [impl->REQ-REST-VERB-ROUTING]\n*539|pub fn route_rest_event(\n 540|    event: RestEvent,\n 541|    allow_remote_fallback: bool,\n 542|    try_local: impl FnOnce() -> Result<Option<EdgeReport>, String>,\n...\n 579|    #[test]\n*580|    fn route_rest_event_contract_table() {\n 581|        use spt_net::net::registry::Status;\n 582|        let miss = || Err(format!(\"info.json absent — {NOT_A_HOSTED_PERCH_MARKER}\"));\n 583|        let no_candidates_expected = || -> Vec<(String, Status)> {\n...\n 588|        assert!(matches!(\n*589|            route_rest_event(RestEvent::Wake, true, || Ok(None), no_candidates_expected),\n 590|            RestRoute::Local(None)\n 591|        ));\n 592|        // Non-marker local error → LocalErr (no fallback), candidates never loaded.\n 593|        assert!(matches!(\n*594|            route_rest_event(\n 595|                RestEvent::Wake,\n 596|                true,\n 597|                || Err(\"registry locked\".into()),\n...\n 603|        assert!(matches!(\n*604|            route_rest_event(RestEvent::Wake, false, miss, no_candidates_expected),\n 605|            RestRoute::LocalErr(_)\n 606|        ));\n 607|        // Daemon-internal event on the fallback path → NoRemoteArm (before load).\n 608|        assert!(matches!(\n*609|            route_rest_event(\n 610|                RestEvent::AutoSuspendTick { now_ms: 0 },\n 611|                true,\n 612|                miss,\n...\n 620|        assert!(matches!(\n*621|            route_rest_event(RestEvent::Wake, true, miss, || one_suspended.clone()),\n 622|            RestRoute::WanAct { node } if node == \"nodeb\"\n 623|        ));\n 624|        let one_active = vec![(\"nodeb\".to_string(), Status::Active)];\n 625|        assert!(matches!(\n*626|            route_rest_event(RestEvent::Wake, true, miss, || one_active.clone()),\n 627|            RestRoute::NoOp { nodes } if nodes == vec![\"nodeb\".to_string()]\n 628|        ));\n 629|        let two_suspended = vec![\n...\n 633|        assert!(matches!(\n*634|            route_rest_event(RestEvent::Wake, true, miss, || two_suspended.clone()),\n 635|            RestRoute::Ambiguous { nodes } if nodes.len() == 2\n 636|        ));\n 637|        assert!(matches!(\n*638|            route_rest_event(RestEvent::Wake, true, miss, Vec::new),\n 639|            RestRoute::NotFound\n 640|        ));\n 641|        // Suspend's ∀-goal: mixed one-active → Act on it.\n...\n 646|        assert!(matches!(\n*647|            route_rest_event(RestEvent::Suspend, true, miss, || mixed.clone()),\n 648|            RestRoute::WanAct { node } if node == \"nodec\"\n 649|        ));\n 650|    }\n#### shellchan.rs\n 167|    // whole payload (no reply header to corrupt the MAC'd line).\n*168|    spt_store::spool::spool_message_at(&perch, \"\", stamped).map_err(|e| format!(\"spool: {e}\"))\n 169|}\n 170|\n 171|/// Drain the shell's pending spool rows down the **stdin** delivery mode:\n#### tunnelhub.rs\n 356|                    .net()\n*357|                    .and_then(|h| h.drain_stream(req.stream_id))\n 358|                    .unwrap_or_default();\n 359|                let res = TunnelRecvResult {\n 360|                    data_b64: encode_bytes(&bytes),\n\n#### pump/\n##### mod.rs\n 194|// [impl->REQ-PUMP-PEER-ISOLATION]\n*195|fn classify_drain_read(read: io::Result<BrokerEvent>) -> DrainStep {\n 196|    match read {\n 197|        Ok(BrokerEvent::NetPresence(ev)) => DrainStep::Apply(ev),\n 198|        Ok(_) => DrainStep::Skip,\n...\n 662|            };\n*663|            match classify_drain_read(events.read_event_until(Some(read_deadline))) {\n 664|                DrainStep::Apply(ev) => handle_presence_event(\n 665|                    ev, &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\n 666|                    &mut pending, &peer_subnets, &ctx, &paths.peer_addrs, round_start,\n...\n 1557|    #[test]\n*1558|    fn drain_read_classifies_dead_carrier_as_restart_timeout_as_quiet() {\n 1559|        // A quiet TimedOut closes the round — it is NEVER a restart.\n 1560|        assert!(matches!(\n*1561|            classify_drain_read(Err(io::Error::new(io::ErrorKind::TimedOut, \"read deadline\"))),\n 1562|            DrainStep::RoundDone\n 1563|        ));\n 1564|        // A dead broker mid-drain: the reader thread ended → UnexpectedEof (or a\n...\n 1566|        assert!(matches!(\n*1567|            classify_drain_read(Err(io::Error::new(\n 1568|                io::ErrorKind::UnexpectedEof,\n 1569|                \"brain IPC reader thread ended (conn closed)\",\n 1570|            ))),\n...\n 1573|        assert!(matches!(\n*1574|            classify_drain_read(Err(io::Error::new(io::ErrorKind::BrokenPipe, \"broker gone\"))),\n 1575|            DrainStep::Restart(_)\n 1576|        ));\n 1577|        // A presence event applies; any other frame kind is skipped.\n...\n 1586|        assert!(matches!(\n*1587|            classify_drain_read(Ok(BrokerEvent::NetPresence(ev))),\n 1588|            DrainStep::Apply(_)\n 1589|        ));\n 1590|        assert!(matches!(\n*1591|            classify_drain_read(Ok(BrokerEvent::NetDialSubmitted)),\n 1592|            DrainStep::Skip\n 1593|        ));\n 1594|    }\n\n### tests/\n#### inject_control_wedge.rs\n 784|        // observe our detach + the child's eventual death and return cleanly.\n*785|        let drain_deadline = std::time::Instant::now() + Duration::from_secs(12);\n*786|        while std::time::Instant::now() < drain_deadline {\n 787|            match operator\n 788|                .read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200)))\n 789|            {\n...\n 1282|    let stop = Arc::new(AtomicBool::new(false));\n*1283|    let drain_stop = Arc::clone(&stop);\n 1284|    let drainer = thread::spawn(move || {\n*1285|        while !drain_stop.load(Ordering::Relaxed) {\n 1286|            let _ =\n 1287|                controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 1288|        }\n...\n 2053|    let stop = Arc::new(AtomicBool::new(false));\n*2054|    let drain_stop = Arc::clone(&stop);\n 2055|    let drainer = thread::spawn(move || {\n*2056|        while !drain_stop.load(Ordering::Relaxed) {\n 2057|            let _ =\n 2058|                controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 2059|        }\n...\n 2673|        let _ = operator.net_stream_send(stream_b, &[], None, true);\n*2674|        let drain_deadline = Instant::now() + Duration::from_secs(12);\n*2675|        while Instant::now() < drain_deadline {\n 2676|            match operator.read_event_until(Some(Instant::now() + Duration::from_millis(200))) {\n 2677|                Ok(BrokerEvent::NetStreamEof { .. }) => break,\n 2678|                Ok(_) => continue,\n...\n 2851|// has NO spt-hosted broker-inject leg (that leg exists ONLY in local cmd_send /\n*2852|// `drain_idle_window`). So a WAN arrival to a relay-less, spt-hosted perch with a\n 2853|// live, healthy translation binary sleeps in the spool FOREVER until some adapter\n 2854|// hook happens to poll — presenting as \"sent but never lands\" on an idle session.\n 2855|//\n...\n 2865|// `broker_socket_name()`, with NO socket parameter and NO `is_running()` seed-ping\n*2866|// dependency (that guard belongs to the CLI-side `drain_idle_window`, not the\n 2867|// in-daemon `receive_wan` path). Safe under the file's process-per-test contract:\n 2868|// this test owns the well-known socket in its own process.\n 2869|//\n...\n 2925|    let stop = Arc::new(AtomicBool::new(false));\n*2926|    let drain_stop = Arc::clone(&stop);\n 2927|    let drainer = thread::spawn(move || {\n*2928|        while !drain_stop.load(Ordering::Relaxed) {\n 2929|            let _ = controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 2930|        }\n 2931|        controller // dropped only after teardown\n...\n 3016|/// endpoint showed ONLINE). The send-time already-idle path AND the pulse-tick re-offer\n*3017|/// belt share this `drain_idle_spool` core; here we drive that core against a REAL broker +\n 3018|/// live translation binary. Binds the broker on the real `broker_socket_name()` (SPT_HOME-\n 3019|/// derived, unique per process) so `try_spt_hosted_inject` reaches THIS broker.\n 3020|///\n*3021|/// RED-FIRST: replace the `drain_idle_spool(...)` call with `0` (the pre-fix parked path —\n 3022|/// no drain ever fires for an already-idle target) and the row stays delivered=0 forever;\n 3023|/// the drain flips it to delivered=1 through the binary (returns 1, stamped idle-inject).\n 3024|// [int->REQ-IDLE-PARKED-DELIVERY]\n...\n 3056|    let stop = Arc::new(AtomicBool::new(false));\n*3057|    let drain_stop = Arc::clone(&stop);\n 3058|    let drainer = thread::spawn(move || {\n*3059|        while !drain_stop.load(Ordering::Relaxed) {\n 3060|            let _ =\n 3061|                controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 3062|        }\n...\n 3073|    // SPOOL the message (the parked row) — NOT injected at send time.\n*3074|    spt_store::spool::spool_message_at(&perch_path, \"peer\", \"parked hello\").expect(\"spool\");\n 3075|\n 3076|    // The parked-idle drain (the shared belt / send-time core): claim + inject via the live\n 3077|    // translation binary. RETRY as the pulse-tick belt does — the binary needs a moment to\n...\n 3082|    for _ in 0..40 {\n*3083|        delivered = spt_daemon::inject::drain_idle_spool(endpoint, &owlery);\n 3084|        if delivered > 0 {\n 3085|            break;\n 3086|        }\n...\n 3150|    let stop = Arc::new(AtomicBool::new(false));\n*3151|    let drain_stop = Arc::clone(&stop);\n 3152|    let drainer = thread::spawn(move || {\n*3153|        while !drain_stop.load(Ordering::Relaxed) {\n 3154|            let _ =\n 3155|                controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 3156|        }\n#### pump.rs\n 836|// supervised restart — is guarded by the DETERMINISTIC unit\n*837|// `pump::tests::drain_read_classifies_dead_carrier_as_restart_timeout_as_quiet`\n 838|// (a real mid-run broker kill is flaky; the unit pins the exact drain-loop\n 839|// classification a refactor must never swallow — doyle's ruling).\n 840|\n#### twohost.rs\n 856|    // ── REMOTE-TRUTH A-3 rung (serve side): A suspends ID_B (setup), then wakes\n*857|    // it with a BARE id — routed at A by the PRODUCTION `route_rest_event`\n 858|    // assembly (seam #2; no `@node` qualifier anywhere) — landing here as an\n 859|    // ordinary wire rest op. The durable rest record is the same observable\n 860|    // rung 8 proved; what is NEW is the bare-id ROUTE that produced the wake\n...\n 1483|    // ── REMOTE-TRUTH A-3 rung (drive side): a BARE id hosted nowhere on this\n*1484|    // node routes through the PRODUCTION assembly (`route_rest_event`, seam #2)\n 1485|    // instead of dying WOKE_FAIL. `try_local` is the REAL local daemon edge\n 1486|    // against A's owlery (a genuine hosted-perch miss — no ID_B perch exists\n 1487|    // here); the candidates are A's REPLICATED registry rows; the decision must\n...\n 1511|    let knob = spt_daemon::DaemonConfig::load().auto_suspend_after_ms;\n*1512|    let route = spt_daemon::route_rest_event(\n 1513|        spt_daemon::RestEvent::Wake,\n 1514|        true,\n 1515|        || spt_daemon::daemon_rest_event(ID_B, spt_daemon::RestEvent::Wake, knob),\n\n## spt-live/src/\n### ingest.rs\n 53|/// structure and content are otherwise preserved. Applied at both strip points\n*54|/// (`resume::append_pending` + `route_slices`) so the marker reaches neither the\n 55|/// presented nor the durable agent context.\n 56|// [impl->REQ-RESUME-CONTEXT-PULL]\n 57|pub fn strip_checkpoint_markers(body: &str) -> String {\n...\n 104|// [impl->REQ-INST-2]\n*105|pub fn route_two_slice(\n 106|    id: &str,\n 107|    project_id: &str,\n 108|    body: &str,\n...\n 112|    let slices = parse_two_slice(body);\n*113|    route_slices(\n 114|        id,\n 115|        project_id,\n 116|        slices.live.as_deref(),\n...\n 122|\n*123|/// [`route_two_slice`] over pre-split (and possibly pre-stamped) slices — the\n 124|/// seam [`crate::signoff::write_resume_commune`] feeds provenance-stamped\n 125|/// bodies through.\n 126|// [impl->REQ-INST-2]\n*127|pub fn route_slices(\n 128|    id: &str,\n 129|    project_id: &str,\n 130|    live: Option<&str>,\n...\n 220|        let slices = parse_two_slice(&body);\n*221|        let writes = route_slices(\n 222|            id,\n 223|            project_id,\n 224|            slices.live.as_deref(),\n...\n 229|        // Un-committable-now ≠ consumed: the project tier is gated on a real\n*230|        // project_id (route_slices), so a non-empty project slice under an\n 231|        // empty id was parsed but never written — preserve it (marker-only\n 232|        // slices route nowhere and are NOT preserved; they carry no content).\n 233|        // [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\n...\n 354|        with_home(|_| {\n*355|            route_two_slice(\n 356|                \"doyle\",\n 357|                \"proj-x\",\n 358|                \"<live-context>\\nbrief !!checkpoint!! do the thing !!checkpoint!!\\n</live-context>\",\n...\n 445|        with_home(|_| {\n*446|            let writes = route_slices(\n 447|                \"doyle\",\n 448|                \"\",\n 449|                Some(\"who I am\"),\n\nShowing files 1-20 of 50+. 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":108,"fileCount":20,"files":["crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/inject.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/notif.rs","crates/spt-daemon/src/presence.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/psyrelay.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/relay.rs","crates/spt-daemon/src/resting.rs","crates/spt-daemon/src/shellchan.rs","crates/spt-daemon/src/tunnelhub.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/twohost.rs","crates/spt-live/src/ingest.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/brainproc.rs","count":5},{"path":"crates/spt-daemon/src/broker.rs","count":4},{"path":"crates/spt-daemon/src/inject.rs","count":3},{"path":"crates/spt-daemon/src/lib.rs","count":1},{"path":"crates/spt-daemon/src/lifecycle.rs","count":1},{"path":"crates/spt-daemon/src/livehost.rs","count":1},{"path":"crates/spt-daemon/src/nethost.rs","count":2},{"path":"crates/spt-daemon/src/notif.rs","count":9},{"path":"crates/spt-daemon/src/presence.rs","count":1},{"path":"crates/spt-daemon/src/projwriter.rs","count":2},{"path":"crates/spt-daemon/src/psyrelay.rs","count":9},{"path":"crates/spt-daemon/src/pump/mod.rs","count":8},{"path":"crates/spt-daemon/src/relay.rs","count":12},{"path":"crates/spt-daemon/src/resting.rs","count":15},{"path":"crates/spt-daemon/src/shellchan.rs","count":1},{"path":"crates/spt-daemon/src/tunnelhub.rs","count":1},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":20},{"path":"crates/spt-daemon/tests/pump.rs","count":1},{"path":"crates/spt-daemon/tests/twohost.rs","count":3},{"path":"crates/spt-live/src/ingest.rs","count":9}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/\n\n### src/\n#### brainproc.rs\n 1462│        // after N polls (the ready-seen latch holds across the undrained polls).\n*1463│        drain_countdown: AtomicU32,\n 1464│    }\n 1465│\n 1466│    impl TestTrialEnv {\n     │...\n 1481│                staged_hash: Mutex::new(None),\n*1482│                drain_countdown: AtomicU32::new(0),\n 1483│            }\n 1484│        }\n 1485│\n     │...\n 1496│        fn drains_after(self, polls: u32) -> Self {\n*1497│            self.drain_countdown.store(polls, Ordering::Relaxed);\n 1498│            self\n 1499│        }\n 1500│\n     │...\n 1546│            // sequentially), so a plain load/store is race-free here.\n*1547│            let left = self.drain_countdown.load(Ordering::Relaxed);\n 1548│            if left == 0 {\n 1549│                return true;\n 1550│            }\n*1551│            self.drain_countdown.store(left - 1, Ordering::Relaxed);\n 1552│            false\n 1553│        }\n 1554│    }\n#### broker.rs\n 2452│        spt_store::perch::resolve_perch_path(endpoint, spt_store::perch::ParentHint::Infer);\n*2453│    if let Err(e) = spt_store::spool::spool_message_at(&perch_path, \"\", envelope) {\n 2454│        eprintln!(\n 2455│            \"TRANSLATION_FAULT_RESPOOL:{endpoint}: failed to re-spool ({e}) — message dropped\"\n 2456│        );\n     │...\n 4632│    /// forever (Windows named pipes reject native read deadlines — see\n*4633│    /// `drain_output_seqs`'s note). Used to PROVE the absence of a trailing marker on\n 4634│    /// a normal teardown: a normal `viewer_writer` exit drops its send-half clone, the\n 4635│    /// client read then hits EOF, and `read_frame` returns Err → `None`.\n 4636│    fn try_read_frame(client: Stream, wait: Duration) -> Option<Envelope> {\n     │...\n 5480│    /// (no per-read deadline: Windows named pipes reject I/O timeouts).\n*5481│    fn drain_output_seqs(client: &mut Stream, n: usize) -> Vec<u64> {\n 5482│        let mut seqs = Vec::with_capacity(n);\n 5483│        while seqs.len() < n {\n 5484│            let env = read_frame(client).expect(\"output frame on the wire\");\n     │...\n 5562│\n*5563│        let seqs = drain_output_seqs(&mut client, 1);\n 5564│        assert_eq!(\n 5565│            seqs,\n 5566│            vec![2],\n#### inject.rs\n 107│// [impl->REQ-HAZARD-DELIVERY-STARVATION]\n*108│pub fn drain_idle_spool(id: &str, owlery: &Path) -> usize {\n 109│    if !is_spt_hosted_no_relay(id, owlery) {\n 110│        return 0;\n 111│    }\n    │...\n 123│    let claimed =\n*124│        match spt_store::spool::claim_idle_edge_audited_at(&perch_path, include_deferred, &audit) {\n 125│            Ok(rows) if !rows.is_empty() => rows,\n 126│            _ => return 0,\n 127│        };\n    │...\n 270│            // Pending row but NOT idle → skip (the broker's active-gate owns *when*).\n*271│            spt_store::spool::spool_message_at(&perch_path, \"bob\", \"hi\").unwrap();\n 272│            assert!(\n 273│                !has_parked_idle_spool(id, &owlery),\n 274│                \"pending but not idle → the belt skips it\"\n#### lib.rs\n 250│    apply_event, arm_transition_echo, daemon_rest_event, effective_auto_suspend, fire_wake_effects,\n*251│    read_rest, request_freshness_pull, route_rest_event, take_freshness_pull, transition,\n 252│    write_rest, EdgeReport, RestEvent, RestRecord, RestRoute, RestState,\n 253│    NOT_A_HOSTED_PERCH_MARKER, PULL_MARKER_FILE,\n 254│};\n#### lifecycle.rs\n 610│        // The project tier routes under the cwd-derived project_id, but an owlery-internal\n*611│        // cwd (psyche-host) routes NO project (empty id → route_slices skips commit_project);\n 612│        // the live/agent slice still commits. A relative dir with no recorded cwd is\n 613│        // SKIPPED loud-once, never guessed.\n 614│        // [impl->REQ-STORE-CONTEXT-BRANCH-FILL]\n#### livehost.rs\n 265│        if crate::inject::has_parked_idle_spool(&id, owlery) {\n*266│            let _ = crate::inject::drain_idle_spool(&id, owlery);\n 267│        }\n 268│    }\n 269│}\n#### nethost.rs\n 1444│    // [impl->REQ-SHELL-4]\n*1445│    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {\n 1446│        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);\n 1447│        let drained = entry.log.lock().unwrap().drain();\n 1448│        // The drain freed ring room — wake a pump that was backpressure-parked.\n     │...\n 1825│        for _ in 0..2000 {\n*1826│            if let Some((mut bytes, fin)) = host.drain_stream(shell) {\n 1827│                got.append(&mut bytes);\n 1828│                if fin {\n 1829│                    finished = true;\n#### notif.rs\n  613│            );\n *614│            let rows = spool::drain_all_at(&ling).unwrap();\n  615│            // Structural carriage (ADR-0020): from=\"issuer\" + body = the typed\n  616│            // <EVENT notify> envelope verbatim — NO legacy reply-to wrapper, NO\n  617│            // double-prefix. The delivery edge passes the typed body through.\n     │...\n  678│            );\n *679│            assert_eq!(spool::drain_all_at(&doyle).unwrap().len(), 1);\n  680│        });\n  681│    }\n  682│\n     │...\n  733│            assert!(\n *734│                spool::drain_all_at(&ling).unwrap().is_empty(),\n  735│                \"nothing delivered locally\"\n  736│            );\n  737│            let stored = s.get(&row.notif_id).unwrap().unwrap();\n     │...\n  750│            );\n *751│            assert_eq!(spool::drain_all_at(&ling).unwrap().len(), 1);\n  752│        });\n  753│    }\n  754│\n     │...\n  831│            );\n *832│            assert_eq!(spool::drain_all_at(&doyle).unwrap().len(), 1);\n  833│\n  834│            // The same boundary again: doyle has now seen it — suppressed,\n  835│            // nothing re-delivered (dismissal is the only further ack).\n     │...\n  850│            );\n *851│            assert!(spool::drain_all_at(&doyle).unwrap().is_empty());\n  852│        });\n  853│    }\n  854│\n     │...\n  978│            );\n *979│            assert_eq!(spool::drain_all_at(&ling).unwrap().len(), 1);\n  980│        });\n  981│    }\n  982│\n     │...\n 1011│            );\n*1012│            assert_eq!(spool::drain_all_at(&ling).unwrap().len(), 1, \"first-fired\");\n 1013│\n 1014│            // Loud = resurfaces: a boundary on a fresh endpoint past the window\n 1015│            // re-delivers the undismissed rollback row (never a one-shot).\n     │...\n 1220│            );\n*1221│            assert!(spool::drain_all_at(&ghost).unwrap().is_empty());\n 1222│        });\n 1223│    }\n 1224│}\n#### presence.rs\n 342│    // [unit->REQ-PRES-1] snapshot loading degrades on junk exactly like the\n*343│    // wansend reader: missing dir, non-json, junk json all read as absent.\n 344│    #[test]\n 345│    fn snapshot_loading_degrades_on_junk() {\n 346│        let dir = tempfile::tempdir().unwrap();\n#### projwriter.rs\n 638│        // unchanged store exits without a scan (the cold/warm start gate).\n*639│        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n 640│        let boot = Coalesced { global: true, ..boot };\n 641│        if let Err(e) = engine.reconcile(&boot, false) {\n 642│            engine.record_failure(e);\n    │...\n 672│            }\n*673│            let req = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n 674│            if due_periodic {\n 675│                since_periodic = Duration::ZERO;\n 676│            }\n#### psyrelay.rs\n 302│            );\n*303│            let rows = spool::drain_all_at(&alice).unwrap();\n 304│            assert_eq!(\n 305│                rows,\n 306│                vec![spool::DrainedMsg {\n    │...\n 334│            // the structural `from` is the re-stamped psyche id, never the spoof.\n*335│            let rows = spool::drain_all_at(&alice).unwrap();\n 336│            assert_eq!(\n 337│                rows,\n 338│                vec![spool::DrainedMsg {\n    │...\n 345│            // The Psyche-addressed target received nothing.\n*346│            assert!(spool::drain_all_at(&victim).unwrap().is_empty());\n 347│        });\n 348│    }\n 349│\n    │...\n 371│            );\n*372│            let rows = spool::drain_all_at(&doyle).unwrap();\n 373│            assert_eq!(rows.len(), 1);\n 374│            // Structural carriage: from = re-stamped psyche id, body = the typed\n 375│            // notify <EVENT> verbatim (no legacy reply-to wrapper, no double-prefix).\n    │...\n 406│            assert!(\n*407│                spool::drain_all_at(&doyle).unwrap().is_empty(),\n 408│                \"nothing delivered now\"\n 409│            );\n 410│            let row = h.store.get(\"cafe:1\").unwrap().unwrap();\n    │...\n 424│            assert_eq!(got, vec![RelayOutcome::DroppedNoReplyTarget]);\n*425│            assert!(spool::drain_all_at(&doyle).unwrap().is_empty());\n 426│        });\n 427│    }\n 428│\n    │...\n 455│            assert!(matches!(err, TurnError::EmptyOutput), \"got {err}\");\n*456│            assert!(spool::drain_all_at(&alice).unwrap().is_empty());\n 457│        });\n 458│    }\n 459│\n    │...\n 466│            assert!(got.is_empty());\n*467│            assert!(spool::drain_all_at(&alice).unwrap().is_empty());\n 468│        });\n 469│    }\n 470│\n    │...\n 496│            assert_eq!(over, vec![RelayOutcome::DroppedOversize]);\n*497│            assert!(spool::drain_all_at(&alice).unwrap().is_empty());\n 498│        });\n 499│    }\n 500│}\n#### relay.rs\n  62│    /// killed one left behind. Returns the number of messages forwarded.\n *63│    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {\n  64│        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg — stamp who drained the row.\n  65│        let audit =\n  66│            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\n *67│        let msgs = spool::drain_non_deferred_audited_at(&self.perch_path, &audit).unwrap_or_default();\n  68│        let n = msgs.len();\n  69│        for m in &msgs {\n  70│            sink(&m.from, &m.body);\n    │...\n  80│        }\n *81│        self.drain_backlog(&mut sink);\n  82│    }\n  83│\n  84│    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as\n    │...\n  89│        // Drain whatever a predecessor relay left behind before streaming live.\n *90│        self.drain_backlog(&mut sink);\n  91│        while !stop.load(Ordering::Relaxed) {\n  92│            self.pump_once(&mut sink);\n  93│        }\n    │...\n 112│            let perch = perch_for(\"doyle\");\n*113│            spool::spool_message_at(&perch, \"alice\", \"m1\").unwrap();\n*114│            spool::spool_message_at(&perch, \"alice\", \"m2\").unwrap();\n 115│\n 116│            // Relay A binds then is \"killed\" (dropped) WITHOUT pumping. The two\n 117│            // messages remain durable in the spool — the relay held no state.\n    │...\n 125│            let mut got = Vec::new();\n*126│            let n = b.drain_backlog(|from, body| got.push((from.to_string(), body.to_string())));\n 127│            assert_eq!(n, 2);\n 128│            assert!(got.iter().all(|(f, _)| f == \"alice\"));\n 129│            assert!(got.iter().any(|(_, b)| b.contains(\"m1\")));\n    │...\n 135│    // [unit->REQ-SEND-WINDOW-DRAIN-HONOR] F-035 GUARD 3 (relay backlog): the relay's\n*136│    // backlog drain (`drain_backlog` → `drain_non_deferred`, `WHERE deferred = 0`)\n 137│    // NEVER forwards an `active_only` row. `active_only` is poll-only — it must never\n 138│    // reach a relay-bearing live agent's idle relay (the field symptom this locks:\n 139│    // an active_only msg surfacing on lia's idle relay). A `default` row on the same\n    │...\n 146│            // default → relay-eligible (idle/wake window); active_only → poll-only.\n*147│            spool::spool_message_at(&perch, \"peer\", \"default-ok\").unwrap();\n*148│            spool::spool_message_deferred_at(&perch, \"peer\", \"active-only-noise\").unwrap();\n 149│\n 150│            let relay = Relay::bind(\"lia\", &perch).unwrap();\n 151│            let mut got = Vec::new();\n*152│            relay.drain_backlog(|from, body| got.push((from.to_string(), body.to_string())));\n 153│\n 154│            assert_eq!(got.len(), 1, \"only the default row rides the relay backlog\");\n 155│            assert_eq!(got[0].1, \"default-ok\");\n    │...\n 175│            let mut got = Vec::new();\n*176│            spool::spool_message_at(&perch, \"alice\", \"hi\").unwrap();\n 177│            relay.run(&stop, |_from, body| got.push(body.to_string()));\n 178│            assert_eq!(got.len(), 1, \"the pre-stream backlog drain still fires\");\n 179│        });\n#### resting.rs\n 471│/// - `spt::api::delivery::poll_drain` (`api poll --include-deferred`,\n*472│///   `drain_all_at`) — **gated**.\n*473│/// - `spt::api::worker::cmd_worker_poll` (`drain_all_at`) — **gated**.\n 474│/// - `spt_daemon::shellchan::deliver_stdin_pending_in` (`peek_all_at` on a\n 475│///   SHELL perch) — not gated: no writer ever defers onto a shell perch\n 476│///   (`spool_shell_frame` spools non-deferred only), and a suspended owner's\n 477│///   online shells are closed by the edge cascade anyway.\n*478│/// - `spt_msg::ring` (`drain_one_at` on the ephemeral reply perch) — not\n 479│///   gated: that perch is recordless by construction (created and deleted\n 480│///   inside one ring call), which IS the legacy fallback; spt-msg also sits\n 481│///   below this crate in the dependency order.\n*482│/// - The event stream (`drain_non_deferred_at`) was never a deferred surface\n 483│///   (KH 1.4) and stays unfiltered-of-this-gate: non-deferred flow while\n 484│///   resting is untouched.\n 485│// [impl->REQ-INST-6]\n    │...\n 538│// [impl->REQ-REST-VERB-ROUTING]\n*539│pub fn route_rest_event(\n 540│    event: RestEvent,\n 541│    allow_remote_fallback: bool,\n 542│    try_local: impl FnOnce() -> Result<Option<EdgeReport>, String>,\n    │...\n 579│    #[test]\n*580│    fn route_rest_event_contract_table() {\n 581│        use spt_net::net::registry::Status;\n 582│        let miss = || Err(format!(\"info.json absent — {NOT_A_HOSTED_PERCH_MARKER}\"));\n 583│        let no_candidates_expected = || -> Vec<(String, Status)> {\n    │...\n 588│        assert!(matches!(\n*589│            route_rest_event(RestEvent::Wake, true, || Ok(None), no_candidates_expected),\n 590│            RestRoute::Local(None)\n 591│        ));\n 592│        // Non-marker local error → LocalErr (no fallback), candidates never loaded.\n 593│        assert!(matches!(\n*594│            route_rest_event(\n 595│                RestEvent::Wake,\n 596│                true,\n 597│                || Err(\"registry locked\".into()),\n    │...\n 603│        assert!(matches!(\n*604│            route_rest_event(RestEvent::Wake, false, miss, no_candidates_expected),\n 605│            RestRoute::LocalErr(_)\n 606│        ));\n 607│        // Daemon-internal event on the fallback path → NoRemoteArm (before load).\n 608│        assert!(matches!(\n*609│            route_rest_event(\n 610│                RestEvent::AutoSuspendTick { now_ms: 0 },\n 611│                true,\n 612│                miss,\n    │...\n 620│        assert!(matches!(\n*621│            route_rest_event(RestEvent::Wake, true, miss, || one_suspended.clone()),\n 622│            RestRoute::WanAct { node } if node == \"nodeb\"\n 623│        ));\n 624│        let one_active = vec![(\"nodeb\".to_string(), Status::Active)];\n 625│        assert!(matches!(\n*626│            route_rest_event(RestEvent::Wake, true, miss, || one_active.clone()),\n 627│            RestRoute::NoOp { nodes } if nodes == vec![\"nodeb\".to_string()]\n 628│        ));\n 629│        let two_suspended = vec![\n    │...\n 633│        assert!(matches!(\n*634│            route_rest_event(RestEvent::Wake, true, miss, || two_suspended.clone()),\n 635│            RestRoute::Ambiguous { nodes } if nodes.len() == 2\n 636│        ));\n 637│        assert!(matches!(\n*638│            route_rest_event(RestEvent::Wake, true, miss, Vec::new),\n 639│            RestRoute::NotFound\n 640│        ));\n 641│        // Suspend's ∀-goal: mixed one-active → Act on it.\n    │...\n 646│        assert!(matches!(\n*647│            route_rest_event(RestEvent::Suspend, true, miss, || mixed.clone()),\n 648│            RestRoute::WanAct { node } if node == \"nodec\"\n 649│        ));\n 650│    }\n#### shellchan.rs\n 167│    // whole payload (no reply header to corrupt the MAC'd line).\n*168│    spt_store::spool::spool_message_at(&perch, \"\", stamped).map_err(|e| format!(\"spool: {e}\"))\n 169│}\n 170│\n 171│/// Drain the shell's pending spool rows down the **stdin** delivery mode:\n#### tunnelhub.rs\n 356│                    .net()\n*357│                    .and_then(|h| h.drain_stream(req.stream_id))\n 358│                    .unwrap_or_default();\n 359│                let res = TunnelRecvResult {\n 360│                    data_b64: encode_bytes(&bytes),\n\n#### pump/\n##### mod.rs\n  194│// [impl->REQ-PUMP-PEER-ISOLATION]\n *195│fn classify_drain_read(read: io::Result<BrokerEvent>) -> DrainStep {\n  196│    match read {\n  197│        Ok(BrokerEvent::NetPresence(ev)) => DrainStep::Apply(ev),\n  198│        Ok(_) => DrainStep::Skip,\n     │...\n  662│            };\n *663│            match classify_drain_read(events.read_event_until(Some(read_deadline))) {\n  664│                DrainStep::Apply(ev) => handle_presence_event(\n  665│                    ev, &mut brain, &mut ops, &mut workers, &due_flags, &mut conns, &mut sched,\n  666│                    &mut pending, &peer_subnets, &ctx, &paths.peer_addrs, round_start,\n     │...\n 1557│    #[test]\n*1558│    fn drain_read_classifies_dead_carrier_as_restart_timeout_as_quiet() {\n 1559│        // A quiet TimedOut closes the round — it is NEVER a restart.\n 1560│        assert!(matches!(\n*1561│            classify_drain_read(Err(io::Error::new(io::ErrorKind::TimedOut, \"read deadline\"))),\n 1562│            DrainStep::RoundDone\n 1563│        ));\n 1564│        // A dead broker mid-drain: the reader thread ended → UnexpectedEof (or a\n     │...\n 1566│        assert!(matches!(\n*1567│            classify_drain_read(Err(io::Error::new(\n 1568│                io::ErrorKind::UnexpectedEof,\n 1569│                \"brain IPC reader thread ended (conn closed)\",\n 1570│            ))),\n     │...\n 1573│        assert!(matches!(\n*1574│            classify_drain_read(Err(io::Error::new(io::ErrorKind::BrokenPipe, \"broker gone\"))),\n 1575│            DrainStep::Restart(_)\n 1576│        ));\n 1577│        // A presence event applies; any other frame kind is skipped.\n     │...\n 1586│        assert!(matches!(\n*1587│            classify_drain_read(Ok(BrokerEvent::NetPresence(ev))),\n 1588│            DrainStep::Apply(_)\n 1589│        ));\n 1590│        assert!(matches!(\n*1591│            classify_drain_read(Ok(BrokerEvent::NetDialSubmitted)),\n 1592│            DrainStep::Skip\n 1593│        ));\n 1594│    }\n\n### tests/\n#### inject_control_wedge.rs\n  784│        // observe our detach + the child's eventual death and return cleanly.\n *785│        let drain_deadline = std::time::Instant::now() + Duration::from_secs(12);\n *786│        while std::time::Instant::now() < drain_deadline {\n  787│            match operator\n  788│                .read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200)))\n  789│            {\n     │...\n 1282│    let stop = Arc::new(AtomicBool::new(false));\n*1283│    let drain_stop = Arc::clone(&stop);\n 1284│    let drainer = thread::spawn(move || {\n*1285│        while !drain_stop.load(Ordering::Relaxed) {\n 1286│            let _ =\n 1287│                controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 1288│        }\n     │...\n 2053│    let stop = Arc::new(AtomicBool::new(false));\n*2054│    let drain_stop = Arc::clone(&stop);\n 2055│    let drainer = thread::spawn(move || {\n*2056│        while !drain_stop.load(Ordering::Relaxed) {\n 2057│            let _ =\n 2058│                controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 2059│        }\n     │...\n 2673│        let _ = operator.net_stream_send(stream_b, &[], None, true);\n*2674│        let drain_deadline = Instant::now() + Duration::from_secs(12);\n*2675│        while Instant::now() < drain_deadline {\n 2676│            match operator.read_event_until(Some(Instant::now() + Duration::from_millis(200))) {\n 2677│                Ok(BrokerEvent::NetStreamEof { .. }) => break,\n 2678│                Ok(_) => continue,\n     │...\n 2851│// has NO spt-hosted broker-inject leg (that leg exists ONLY in local cmd_send /\n*2852│// `drain_idle_window`). So a WAN arrival to a relay-less, spt-hosted perch with a\n 2853│// live, healthy translation binary sleeps in the spool FOREVER until some adapter\n 2854│// hook happens to poll — presenting as \"sent but never lands\" on an idle session.\n 2855│//\n     │...\n 2865│// `broker_socket_name()`, with NO socket parameter and NO `is_running()` seed-ping\n*2866│// dependency (that guard belongs to the CLI-side `drain_idle_window`, not the\n 2867│// in-daemon `receive_wan` path). Safe under the file's process-per-test contract:\n 2868│// this test owns the well-known socket in its own process.\n 2869│//\n     │...\n 2925│    let stop = Arc::new(AtomicBool::new(false));\n*2926│    let drain_stop = Arc::clone(&stop);\n 2927│    let drainer = thread::spawn(move || {\n*2928│        while !drain_stop.load(Ordering::Relaxed) {\n 2929│            let _ = controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 2930│        }\n 2931│        controller // dropped only after teardown\n     │...\n 3016│/// endpoint showed ONLINE). The send-time already-idle path AND the pulse-tick re-offer\n*3017│/// belt share this `drain_idle_spool` core; here we drive that core against a REAL broker +\n 3018│/// live translation binary. Binds the broker on the real `broker_socket_name()` (SPT_HOME-\n 3019│/// derived, unique per process) so `try_spt_hosted_inject` reaches THIS broker.\n 3020│///\n*3021│/// RED-FIRST: replace the `drain_idle_spool(...)` call with `0` (the pre-fix parked path —\n 3022│/// no drain ever fires for an already-idle target) and the row stays delivered=0 forever;\n 3023│/// the drain flips it to delivered=1 through the binary (returns 1, stamped idle-inject).\n 3024│// [int->REQ-IDLE-PARKED-DELIVERY]\n     │...\n 3056│    let stop = Arc::new(AtomicBool::new(false));\n*3057│    let drain_stop = Arc::clone(&stop);\n 3058│    let drainer = thread::spawn(move || {\n*3059│        while !drain_stop.load(Ordering::Relaxed) {\n 3060│            let _ =\n 3061│                controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 3062│        }\n     │...\n 3073│    // SPOOL the message (the parked row) — NOT injected at send time.\n*3074│    spt_store::spool::spool_message_at(&perch_path, \"peer\", \"parked hello\").expect(\"spool\");\n 3075│\n 3076│    // The parked-idle drain (the shared belt / send-time core): claim + inject via the live\n 3077│    // translation binary. RETRY as the pulse-tick belt does — the binary needs a moment to\n     │...\n 3082│    for _ in 0..40 {\n*3083│        delivered = spt_daemon::inject::drain_idle_spool(endpoint, &owlery);\n 3084│        if delivered > 0 {\n 3085│            break;\n 3086│        }\n     │...\n 3150│    let stop = Arc::new(AtomicBool::new(false));\n*3151│    let drain_stop = Arc::clone(&stop);\n 3152│    let drainer = thread::spawn(move || {\n*3153│        while !drain_stop.load(Ordering::Relaxed) {\n 3154│            let _ =\n 3155│                controller.read_event_until(Some(Instant::now() + Duration::from_millis(100)));\n 3156│        }\n#### pump.rs\n 836│// supervised restart — is guarded by the DETERMINISTIC unit\n*837│// `pump::tests::drain_read_classifies_dead_carrier_as_restart_timeout_as_quiet`\n 838│// (a real mid-run broker kill is flaky; the unit pins the exact drain-loop\n 839│// classification a refactor must never swallow — doyle's ruling).\n 840│\n#### twohost.rs\n  856│    // ── REMOTE-TRUTH A-3 rung (serve side): A suspends ID_B (setup), then wakes\n *857│    // it with a BARE id — routed at A by the PRODUCTION `route_rest_event`\n  858│    // assembly (seam #2; no `@node` qualifier anywhere) — landing here as an\n  859│    // ordinary wire rest op. The durable rest record is the same observable\n  860│    // rung 8 proved; what is NEW is the bare-id ROUTE that produced the wake\n     │...\n 1483│    // ── REMOTE-TRUTH A-3 rung (drive side): a BARE id hosted nowhere on this\n*1484│    // node routes through the PRODUCTION assembly (`route_rest_event`, seam #2)\n 1485│    // instead of dying WOKE_FAIL. `try_local` is the REAL local daemon edge\n 1486│    // against A's owlery (a genuine hosted-perch miss — no ID_B perch exists\n 1487│    // here); the candidates are A's REPLICATED registry rows; the decision must\n     │...\n 1511│    let knob = spt_daemon::DaemonConfig::load().auto_suspend_after_ms;\n*1512│    let route = spt_daemon::route_rest_event(\n 1513│        spt_daemon::RestEvent::Wake,\n 1514│        true,\n 1515│        || spt_daemon::daemon_rest_event(ID_B, spt_daemon::RestEvent::Wake, knob),\n\n## spt-live/src/\n### ingest.rs\n  53│/// structure and content are otherwise preserved. Applied at both strip points\n *54│/// (`resume::append_pending` + `route_slices`) so the marker reaches neither the\n  55│/// presented nor the durable agent context.\n  56│// [impl->REQ-RESUME-CONTEXT-PULL]\n  57│pub fn strip_checkpoint_markers(body: &str) -> String {\n    │...\n 104│// [impl->REQ-INST-2]\n*105│pub fn route_two_slice(\n 106│    id: &str,\n 107│    project_id: &str,\n 108│    body: &str,\n    │...\n 112│    let slices = parse_two_slice(body);\n*113│    route_slices(\n 114│        id,\n 115│        project_id,\n 116│        slices.live.as_deref(),\n    │...\n 122│\n*123│/// [`route_two_slice`] over pre-split (and possibly pre-stamped) slices — the\n 124│/// seam [`crate::signoff::write_resume_commune`] feeds provenance-stamped\n 125│/// bodies through.\n 126│// [impl->REQ-INST-2]\n*127│pub fn route_slices(\n 128│    id: &str,\n 129│    project_id: &str,\n 130│    live: Option<&str>,\n    │...\n 220│        let slices = parse_two_slice(&body);\n*221│        let writes = route_slices(\n 222│            id,\n 223│            project_id,\n 224│            slices.live.as_deref(),\n    │...\n 229│        // Un-committable-now ≠ consumed: the project tier is gated on a real\n*230│        // project_id (route_slices), so a non-empty project slice under an\n 231│        // empty id was parsed but never written — preserve it (marker-only\n 232│        // slices route nowhere and are NOT preserved; they carry no content).\n 233│        // [impl->REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE]\n    │...\n 354│        with_home(|_| {\n*355│            route_two_slice(\n 356│                \"doyle\",\n 357│                \"proj-x\",\n 358│                \"<live-context>\\nbrief !!checkpoint!! do the thing !!checkpoint!!\\n</live-context>\",\n    │...\n 445│        with_home(|_| {\n*446│            let writes = route_slices(\n 447│                \"doyle\",\n 448│                \"\",\n 449│                Some(\"who I am\"),"},"isError":false,"timestamp":1784197385418}}
{"type":"message","id":"4106aae8","parentId":"daf635b0","timestamp":"2026-07-16T10:23:05.424Z","message":{"role":"toolResult","toolCallId":"call_X8zFRiAL7B2eJiCkOymcIBl5|fc_017bff223290b7ba016a58b1095d24819a86a50c9931500dcd","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## autostart.rs\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## brain.rs\n 1845|        let name = unique_name();\n*1846|        let listener = LocalSocketTransport::bind(&name).expect(\"bind stub listener\");\n 1847|\n 1848|        // The brain connects on a thread (cold_start blocks on connect+hello).\n 1849|        let connect_name = name.clone();\n...\n 1852|\n*1853|        let mut server = listener.accept().expect(\"accept the brain\");\n 1854|        crate::transport::recv_hello(&mut server, Role::Brain).expect(\"stub recv_hello\");\n 1855|        let brain = brain_handle.join().expect(\"brain thread\");\n 1856|\n## brainproc.rs\n 240|    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n*241|    // shell waker). Online-only is staged behind the interim `api listen`\n 242|    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n 243|    // brain the first-host. The stop flag is held for symmetry; the threads die\n 244|    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n## broker.rs\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...\n 2511|\n*2512|/// The broker process kernel. Owns the listening socket, the table of hosted\n 2513|/// sessions, and the broker-owned idempotency anchor (B5); holds no logic state\n 2514|/// (ADR-0004 §B). The [`EffectJournal`] is a §B resource — the durable\n 2515|/// recovery anchor a brain crash must not lose, so it lives in the broker, not\n...\n 2517|pub struct Broker {\n*2518|    listener: LocalSocketTransport,\n*2519|    /// The namespaced listener name — held so [`Broker::stop`] can dial a throwaway\n 2520|    /// connection to unblock the blocked `accept()` in [`serve`](Self::serve) (there is\n 2521|    /// no other wakeup for the accept loop).\n 2522|    name: String,\n...\n 2646|impl Broker {\n*2647|    /// Bind the broker's listening endpoint at the namespaced `name`, with the\n 2648|    /// effect journal at the default path (`<spt_home>/daemon-effects.log`).\n 2649|    pub fn bind(name: &str) -> io::Result<Arc<Self>> {\n 2650|        Self::bind_in(name, default_journal_path())\n...\n 2659|    /// Bind with an already-started broker-owned network host (D4a). Taking the\n*2660|    /// host (not its config) keeps a net bind failure separable from a listener\n 2661|    /// bind failure — the daemon degrades to a net-less broker rather than\n 2662|    /// losing PTY hosting (see `Daemon::run`).\n 2663|    // [impl->REQ-DAEMON-2]\n...\n 2688|        Ok(Arc::new(Broker {\n*2689|            listener: LocalSocketTransport::bind(name)?,\n 2690|            name: name.to_string(),\n 2691|            stop: AtomicBool::new(false),\n 2692|            sessions: Arc::new(Mutex::new(HashMap::new())),\n...\n 2742|        loop {\n*2743|            let conn = self.listener.accept()?;\n 2744|            // C1 graceful stop: [`Broker::stop`] raises the latch then dials the\n 2745|            // throwaway connection that woke this accept — observe it and return so the\n*2746|            // serve thread (and, with it, the listener) tears down instead of leaking.\n 2747|            if self.stop.load(Ordering::Acquire) {\n 2748|                return Ok(());\n 2749|            }\n...\n 2758|    /// dial one throwaway connection so the blocked `accept()` wakes, observes the\n*2759|    /// latch, and returns (dropping the listener). Best-effort + idempotent — a second\n 2760|    /// call, or a call after the loop already exited, just fails the dial harmlessly.\n 2761|    /// Fills the honest gap that `serve` is otherwise unstoppable (the leaked-serve\n 2762|    /// load that starved the seed stop barrier — REQ-HAZARD-DAEMON-STOP-BARRIER).\n...\n 2771|    pub fn serve_once(self: &Arc<Self>) -> io::Result<JoinHandle<()>> {\n*2772|        let conn = self.listener.accept()?;\n 2773|        let me = Arc::clone(self);\n 2774|        Ok(thread::spawn(move || {\n 2775|            let _ = me.handle_conn(conn);\n...\n 2820|        );\n*2821|        let Ok(listener) = LocalSocketTransport::bind(&name) else {\n 2822|            return false;\n 2823|        };\n 2824|        let Ok(_client) = LocalSocketTransport::connect(&name) else {\n...\n 2826|        };\n*2827|        let Ok(server) = listener.accept() else {\n 2828|            return false;\n 2829|        };\n 2830|        let (_recv, send_half) = server.split();\n...\n 3669|    /// The endpoint-keyed counterpart of [`dispatch_input`] — a CLI `spt send` to an\n*3670|    /// spt-hosted endpoint (broker holds its PTY, no `api listen` relay) lands here.\n 3671|    /// Resolution is atomic under the sessions lock (no TOCTOU); the bytes are a\n 3672|    /// pre-rendered `<EVENT>` envelope (the caller owns the wire format — ADR-0020).\n 3673|    /// Replies [`KIND_ENDPOINT_INJECTED`] with `delivered` so the caller knows\n...\n 5449|\n*5450|    /// A connected local-socket pair for the unit: bind a listener, connect a\n 5451|    /// client `Stream`, accept the server `Stream`. Returns the SERVER send half\n 5452|    /// wrapped as a `SharedSend` (the broker side a `become_controller` writes\n 5453|    /// through) plus the CLIENT `Stream` (the brain side we read the wire off) and\n...\n 5465|        );\n*5466|        let listener = LocalSocketTransport::bind(&name).expect(\"bind listener\");\n 5467|        let client = LocalSocketTransport::connect(&name).expect(\"connect client\");\n*5468|        let server = listener.accept().expect(\"accept server side\");\n 5469|        let (recv, send) = server.split();\n 5470|        (\n 5471|            Arc::new(BrokerConn::new(send, brain_write_deadline())),\n## conn.rs\n 696|        );\n*697|        let listener = LocalSocketTransport::bind(&name).expect(\"bind\");\n 698|        let client = LocalSocketTransport::connect(&name).expect(\"connect\");\n*699|        let server = listener.accept().expect(\"accept\");\n 700|        let (_recv, send) = server.split();\n 701|        (\n 702|            BrokerConn::new(send, Duration::from_millis(2000)),\n## daemon.rs\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## deadline.rs\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## digesthub.rs\n 175|pub fn serve_digest_control(name: &str, hub: Arc<DigestHub>) -> io::Result<()> {\n*176|    let listener = LocalSocketTransport::bind(name)?;\n 177|    loop {\n*178|        let conn = listener.accept()?;\n 179|        let hub = Arc::clone(&hub);\n 180|        thread::spawn(move || {\n 181|            let _ = handle_conn(conn, &hub);\n## docshost.rs\n 216|\n*217|/// Bind the docs listener(s): `127.0.0.1:port` is REQUIRED (a failure is the\n 218|/// caller's loud log); `[::1]:port` is best-effort where the stack offers it.\n 219|/// Returns the bound v4 address (the resolved-port source for tests binding\n 220|/// port 0).\n*221|async fn bind_loopback(port: u16) -> io::Result<(tokio::net::TcpListener, Option<tokio::net::TcpListener>)> {\n*222|    let v4 = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port))).await?;\n 223|    let actual = v4.local_addr()?.port();\n*224|    let v6 = tokio::net::TcpListener::bind((\"::1\", actual)).await.ok();\n 225|    Ok((v4, v6))\n 226|}\n 227|\n*228|async fn accept_loop(listener: tokio::net::TcpListener, docs_root: PathBuf) {\n 229|    loop {\n*230|        let Ok((stream, _peer)) = listener.accept().await else {\n 231|            continue;\n 232|        };\n 233|        let io = hyper_util::rt::TokioIo::new(stream);\n## drivehub.rs\n 181|pub fn serve_drive_control(name: &str, hub: Arc<DriveHub>) -> io::Result<()> {\n*182|    let listener = LocalSocketTransport::bind(name)?;\n 183|    loop {\n*184|        let conn = listener.accept()?;\n 185|        let hub = Arc::clone(&hub);\n 186|        thread::spawn(move || {\n 187|            let _ = handle_conn(conn, &hub);\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 28|\n*29|/// The seed-control listener name (B6): the channel `api seed`/`api listen`\n 30|/// connect to for the in-memory seed map. It is also the daemon's liveness\n 31|/// anchor — present for the daemon's whole lifetime.\n 32|pub fn seed_socket_name() -> String {\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 40|\n*41|/// The digest-control listener name (B8): the channel `spt digest <id>` connects\n 42|/// to for a snapshot pull or a delta-stream subscribe (ADR-0008 / REQ-TERM-4).\n 43|pub fn digest_socket_name() -> String {\n 44|    format!(\"spt-daemon-digest-{}.sock\", home_tag())\n...\n 46|\n*47|/// The drive-control listener name (M11-W2): the channel `spt shell drive` (owner\n 48|/// write) and `api drive-poll --link` (shell take-and-clear) connect to for the\n 49|/// per-shell ephemeral drive slot (REQ-SHELL-3). Broker-served like the digest\n 50|/// channel; the slot is in-memory and dies with the daemon.\n...\n 55|\n*56|/// The tunnel-control listener name (M11-W3): the channel `spt shell tunnel` (owner\n 57|/// duplex) and the shell binary's `api tunnel` connect to for the per-shell opaque\n 58|/// byte tunnel registry (REQ-SHELL-4). Broker-served like the drive channel; the\n 59|/// registry holds only held stream ids + the link-token stamp and dies with the\n## harnesshost.rs\n 5|//! launcher runs the adapter's `[session.self]` command and the harness binds its\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## inject.rs\n 4|//! PTY is broker-owned, it is online + broker-PTY-controllable) with NO `api\n*5|//! listen` relay (no advertised TCP address). These are exactly the endpoints a\n 6|//! plain delivery would otherwise SILENTLY SPOOL — `deliver_tcp` finds no address\n 7|//! — so they must instead be reached by injecting the `<EVENT>` into the\n 8|//! broker-held PTY through the live translation binary.\n...\n 29|/// Whether `target` is an spt-hosted endpoint whose PTY the broker holds, with NO\n*30|/// `api listen` relay: it is online, has no resolvable TCP relay address, and is\n 31|/// broker-PTY-`controllable`. A harness-hosted endpoint with a relay\n 32|/// (`resolve_address` is `Some`) takes the normal TCP path; an offline or\n 33|/// non-controllable perch is not a PTY-inject target. Factored verbatim from\n...\n 41|    if spt_store::registry::resolve_address(target, owlery).is_some() {\n*42|        return false; // a relay listener exists → normal TCP delivery\n 43|    }\n 44|    spt_store::info::read_info(&spt_store::perch::resolve_perch_path(\n 45|        target,\n...\n 188|        info::write_info(&perch_path, &rec).unwrap();\n*189|        std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 190|        info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 191|        perch_path\n 192|    }\n...\n 210|\n*211|            // A relay listener registered → normal TCP delivery, NOT inject.\n 212|            let addr: std::net::SocketAddr = \"127.0.0.1:6553\".parse().unwrap();\n 213|            spt_store::registry::register_address(id, &addr, &owlery).unwrap();\n 214|            assert!(\n...\n 286|                !has_parked_idle_spool(id, &owlery),\n*287|                \"a relay endpoint delivers via its own listener, not the belt\"\n 288|            );\n 289|        });\n 290|    }\n## lib.rs\n 7|//!   live consumer would lose continuity on if the logic restarted — PTY\n*8|//!   masters (from [`spt_term`]), spawned harness children, accepted/listening\n 9|//!   sockets — and runs no logic;\n 10|//! - a **brain** (restartable logic) holds routing, registry, manifest parse,\n 11|//!   and the re-hosted [`spt_live`] lifecycle, attaching to the broker over a\n...\n 28|//! **B3 (this commit)** consolidates the interim no-daemon model into the brain:\n*29|//! the `api listen` pulse / Psyche / drop-ingest loops become scheduled\n 30|//! in-process loops ([`lifecycle::BrainLifecycle`]) paced by a durable,\n 31|//! configurable period ([`config::DaemonConfig`]) instead of the interim 5s\n 32|//! constant; the only residue left in the harness-hosted session is the thin,\n## lifecycle.rs\n 1|//! The brain's LiveAgent lifecycle host (B3) — the consolidated re-host of the\n*2|//! interim `api listen` runner's live half.\n 3|//!\n*4|//! In the no-daemon interim model the long-lived `spt api listen` process hosted\n 5|//! the LiveAgent lifecycle (per-event psyche turns, pulse-driven drop ingest, echo\n 6|//! fires, graceful signoff) directly in the agent's session. Consolidation\n 7|//! (REQ-DAEMON-1) moves that orchestration **down into `spt-daemon`** so the\n...\n 1268|            || {\n*1269|                let ready = perch::resolve_ready_file(&self.id, ParentHint::Infer);\n 1270|                if let Err(e) = std::fs::remove_file(&ready) {\n 1271|                    if e.kind() != std::io::ErrorKind::NotFound {\n 1272|                        teardown_err = Some(e.to_string());\n...\n 1328|        // 3.2 — sweep a stale signoff sentinel before supervising, exactly as a\n*1329|        // listener start does, so a prior crash's sentinel can't self-trigger.\n 1330|        self.sweep_on_start();\n 1331|        while !stop.load(Ordering::Relaxed) {\n 1332|            if !is_process_alive(owner_pid) {\n...\n 1415|        write_info(&perch_path, &info).unwrap();\n*1416|        let ready = perch::resolve_ready_file(id, ParentHint::Infer);\n 1417|        if let Some(p) = ready.parent() {\n 1418|            std::fs::create_dir_all(p).unwrap();\n 1419|        }\n...\n 2508|\n*2509|            let ready = perch::resolve_ready_file(\"doyle\", ParentHint::Infer);\n 2510|            std::fs::create_dir_all(ready.parent().unwrap()).unwrap();\n 2511|            std::fs::write(&ready, \"\").unwrap();\n 2512|\n...\n 2540|\n*2541|            let ready = perch::resolve_ready_file(\"doyle\", ParentHint::Infer);\n 2542|            std::fs::create_dir_all(ready.parent().unwrap()).unwrap();\n 2543|            std::fs::write(&ready, \"\").unwrap();\n 2544|\n...\n 2610|            seed_supervised_perch(\"doyle\", DEAD_PID);\n*2611|            let ready = perch::resolve_ready_file(\"doyle\", ParentHint::Infer);\n 2612|            assert!(ready.exists(), \"precondition: ready marker present\");\n 2613|\n 2614|            let stop = AtomicBool::new(false);\n...\n 2640|            seed_supervised_perch(\"doyle\", std::process::id());\n*2641|            let ready = perch::resolve_ready_file(\"doyle\", ParentHint::Infer);\n 2642|\n 2643|            // Owner is this (alive) process; stop is already set → loop exits with\n 2644|            // no signoff and leaves the perch untouched.\n## linkhost.rs\n 211|\n*212|/// At shell-online (the `api bind-shell` seam), open the shell's opaque tunnel if\n 213|/// its manifest enables it (M11-W3, REQ-SHELL-4). Resolves the shell's `[shell]`\n 214|/// body and, when `[shell.tunnel].enable`, ensures the broker-held loopback stream\n 215|/// pair under the CURRENT link token — so the tunnel's lifetime is the link's\n## livehost.rs\n 15|//!\n*16|//! **Online-only is staged behind the interim.** The interim `spt api listen`\n 17|//! process still first-hosts its live agents today; those perches are\n 18|//! `status`-ABSENT (not \"online\"), so this scan ignores them — no double-host in\n 19|//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the\n*20|//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this\n 21|//! reconcile reacts within one tick, the same way the shell waker reacts to a\n 22|//! CLI-process status flip. The full bind→online→brain-restart→re-host arc is\n 23|//! proven through the real brain process by the W0.4 production int.\n...\n 141|///\n*142|/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are\n 143|/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.\n 144|// [impl->REQ-DAEMON-1]\n 145|pub fn reconcile_once(\n...\n 658|/// (spt-hosted, broker-PTY) perches are reconciled here. A HARNESS-HOSTED relay\n*659|/// live agent (`api listen`, `controllable == Some(false)`) is legitimately online\n 660|/// with NO broker session (its liveness is the relay + ready marker), so it is\n 661|/// NEVER marked offline by this pass. A `controllable == None` perch (legacy /\n 662|/// dead-psyche like wall-a) is also exempt here — its liveness/phantom class is\n...\n 887|    let _ = std::fs::remove_file(psyche_perch.join(\"info.json\"));\n*888|    let _ = std::fs::remove_file(perch::resolve_ready_file(&psyche_id, ParentHint::Explicit(id)));\n 889|    eprintln!(\n 890|        \"LEGACY_PSYCHE_SWEEP_REAP:{id} pid={pid}: reaped stranded pre-W3 resident psyche \\\n 891|         + cleared its stale `{psyche_id}` ready registration\"\n...\n 2067|    #[test]\n*2068|    fn reconcile_attempts_resume_for_a_registered_woken_endpoint() {\n 2069|        with_home(|_| {\n 2070|            let drops = perch::spt_home().join(\"drops\");\n 2071|            std::fs::create_dir_all(&drops).unwrap();\n## nethost.rs\n 811|    /// per key — the ALPN is the pre-trust/trusted boundary (ADR-0005).\n*812|    /// The meet-rotation listener loop rides the same runtime.\n 813|    // [impl->REQ-DAEMON-2]\n 814|    // [impl->REQ-SUBNET-2]\n 815|    pub fn start(cfg: NetConfig) -> io::Result<NetHost> {\n## notif.rs\n 313|/// REQ-NOTIF-2's M5 generalization): render a surfaced notif through every\n*314|/// registered shell adapter that (a) has an instance attached to `endpoint`\n 315|/// and (b) declares the template. Entirely manifest-driven — spt-core never\n 316|/// names a capability or knows what \"render\" means; the adapter's template is\n 317|/// the whole contract (`spt notify` → presence MRA → the winning node spawns\n...\n 696|                \"ling\",\n*697|                spt_net::net::registry::Instance {\n 698|                    node: \"remote-hex\".into(),\n 699|                    status: spt_net::net::registry::Status::Active,\n 700|                    epoch: 1,\n## pairhost.rs\n 10|//!   subnet-global [`PairingRateLimiter`] (red-team #11) is the standing-\n*11|//!   listener guard; [`respond`] additionally sheds concurrent ceremonies at\n 12|//!   the door (`try_lock`) and bounds each admitted ceremony to the\n 13|//!   limiter's own [`CEREMONY_TIMEOUT_SECS`] so a stalling peer cannot wedge\n 14|//!   the responder (the limiter's stale-slot reclaim is the backstop).\n 15|//! - **Meet rotation** ([`spawn_meet_rotation`]): for each member subnet, a\n*16|//!   short-lived rendezvous listener bound to the token-derived identity\n 17|//!   (`pairing::meet`), re-bound every TOTP step — how a joiner that knows\n 18|//!   nothing but (name, code) finds this responder over LAN or relay.\n 19|//! - **Joiner leg** ([`join_ceremony`]): dial via rendezvous, run the\n...\n 157|/// The meet-rotation loop: for every member subnet, keep a rendezvous meet\n*158|/// listener bound under the (name, current-step)-derived identity, answering\n 159|/// joiner probes with `real_addr` — the responder's actual pairing-endpoint\n 160|/// address. Re-binds at each TOTP step boundary (window rotation = re-bind,\n 161|/// `pairing::meet` docs); re-reads the subnet store per window so a freshly\n...\n 180|    handle.spawn(async move {\n*181|        // The PREVIOUS window's listeners, kept serving one extra period\n*182|        // (M7 acceptance fix): published records outlive listeners by the\n 183|        // DNS TTL, so a joiner resolving the step-1 token must find a live\n*184|        // listener there, not a ghost. Overlap = every published id is\n 185|        // dialable for its full discoverable life.\n 186|        let mut prev_tasks: Vec<tokio::task::JoinHandle<()>> = Vec::new();\n 187|        loop {\n...\n 189|            // Serve-state filter (REQ-SUBNET-5): a detached subnet's meet\n*190|            // listener does not rotate — a joiner cannot rendezvous with a\n 191|            // membership this node deliberately is not serving. Re-read per\n 192|            // window like the subnet store itself.\n 193|            let attachment = spt_store::attachment::AttachmentStore::load();\n...\n 199|                .collect();\n*200|            // One listener task per subnet for this window's lifetime.\n 201|            let mut window_tasks = Vec::new();\n 202|            for name in names {\n 203|                let relay = relay.clone();\n...\n 205|                window_tasks.push(tokio::spawn(async move {\n*206|                    let listener = match bind_rendezvous(&name, step, relay, discovery, scope).await\n 207|                    {\n 208|                        Ok(l) => l,\n 209|                        Err(e) => {\n...\n 214|                    // Window diagnostics (live-debugging the meet path needs\n*215|                    // the derived id + what this listener actually advertises\n*216|                    // — an undiscoverable listener is indistinguishable from\n 217|                    // a dead one without this line).\n 218|                    eprintln!(\n 219|                        \"PAIR_MEET_UP:{name} step={step} id={} addr={:?}\",\n*220|                        listener.endpoint().node_id_hex(),\n*221|                        listener.endpoint().addr()\n 222|                    );\n 223|                    // Serve probes until the window task is aborted at the\n 224|                    // step boundary; each failure only ends one exchange.\n 225|                    loop {\n*226|                        match listener.serve_once(&real_addr).await {\n 227|                            Ok(()) => eprintln!(\"PAIR_MEET_ANSWERED:{name} step={step}\"),\n 228|                            Err(e) => {\n 229|                                eprintln!(\"PAIR_MEET_SERVE_ERR:{name}: {e}\");\n...\n 257|pub enum JoinFail {\n*258|    /// No meet listener answered for any window token — no seed-holder\n 259|    /// reachable under that subnet name right now.\n 260|    NoSeedHolder(String),\n 261|    /// The ceremony ran and the confirmation failed — in practice: wrong code.\n\nShowing files 1-20 of 116+. 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":84,"fileCount":20,"files":["crates/spt-daemon/src/autostart.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/broker.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deadline.rs","crates/spt-daemon/src/digesthub.rs","crates/spt-daemon/src/docshost.rs","crates/spt-daemon/src/drivehub.rs","crates/spt-daemon/src/endpoint.rs","crates/spt-daemon/src/harnesshost.rs","crates/spt-daemon/src/inject.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/linkhost.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/nethost.rs","crates/spt-daemon/src/notif.rs","crates/spt-daemon/src/pairhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/autostart.rs","count":1},{"path":"crates/spt-daemon/src/brain.rs","count":2},{"path":"crates/spt-daemon/src/brainproc.rs","count":1},{"path":"crates/spt-daemon/src/broker.rs","count":17},{"path":"crates/spt-daemon/src/conn.rs","count":2},{"path":"crates/spt-daemon/src/daemon.rs","count":1},{"path":"crates/spt-daemon/src/deadline.rs","count":1},{"path":"crates/spt-daemon/src/digesthub.rs","count":2},{"path":"crates/spt-daemon/src/docshost.rs","count":6},{"path":"crates/spt-daemon/src/drivehub.rs","count":2},{"path":"crates/spt-daemon/src/endpoint.rs","count":6},{"path":"crates/spt-daemon/src/harnesshost.rs","count":1},{"path":"crates/spt-daemon/src/inject.rs","count":6},{"path":"crates/spt-daemon/src/lib.rs","count":2},{"path":"crates/spt-daemon/src/lifecycle.rs","count":9},{"path":"crates/spt-daemon/src/linkhost.rs","count":1},{"path":"crates/spt-daemon/src/livehost.rs","count":6},{"path":"crates/spt-daemon/src/nethost.rs","count":1},{"path":"crates/spt-daemon/src/notif.rs","count":2},{"path":"crates/spt-daemon/src/pairhost.rs","count":15}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## autostart.rs\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## brain.rs\n 1845│        let name = unique_name();\n*1846│        let listener = LocalSocketTransport::bind(&name).expect(\"bind stub listener\");\n 1847│\n 1848│        // The brain connects on a thread (cold_start blocks on connect+hello).\n 1849│        let connect_name = name.clone();\n     │...\n 1852│\n*1853│        let mut server = listener.accept().expect(\"accept the brain\");\n 1854│        crate::transport::recv_hello(&mut server, Role::Brain).expect(\"stub recv_hello\");\n 1855│        let brain = brain_handle.join().expect(\"brain thread\");\n 1856│\n## brainproc.rs\n 240│    // every ONLINE live endpoint from disk (the harness-endpoint mirror of the\n*241│    // shell waker). Online-only is staged behind the interim `api listen`\n 242│    // (status-absent perches are skipped — no double-host) until W0.2 makes the\n 243│    // brain the first-host. The stop flag is held for symmetry; the threads die\n 244│    // with the brain and the next brain rehydrates from disk (CONTEXT:194).\n## broker.rs\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     │...\n 2511│\n*2512│/// The broker process kernel. Owns the listening socket, the table of hosted\n 2513│/// sessions, and the broker-owned idempotency anchor (B5); holds no logic state\n 2514│/// (ADR-0004 §B). The [`EffectJournal`] is a §B resource — the durable\n 2515│/// recovery anchor a brain crash must not lose, so it lives in the broker, not\n     │...\n 2517│pub struct Broker {\n*2518│    listener: LocalSocketTransport,\n*2519│    /// The namespaced listener name — held so [`Broker::stop`] can dial a throwaway\n 2520│    /// connection to unblock the blocked `accept()` in [`serve`](Self::serve) (there is\n 2521│    /// no other wakeup for the accept loop).\n 2522│    name: String,\n     │...\n 2646│impl Broker {\n*2647│    /// Bind the broker's listening endpoint at the namespaced `name`, with the\n 2648│    /// effect journal at the default path (`<spt_home>/daemon-effects.log`).\n 2649│    pub fn bind(name: &str) -> io::Result<Arc<Self>> {\n 2650│        Self::bind_in(name, default_journal_path())\n     │...\n 2659│    /// Bind with an already-started broker-owned network host (D4a). Taking the\n*2660│    /// host (not its config) keeps a net bind failure separable from a listener\n 2661│    /// bind failure — the daemon degrades to a net-less broker rather than\n 2662│    /// losing PTY hosting (see `Daemon::run`).\n 2663│    // [impl->REQ-DAEMON-2]\n     │...\n 2688│        Ok(Arc::new(Broker {\n*2689│            listener: LocalSocketTransport::bind(name)?,\n 2690│            name: name.to_string(),\n 2691│            stop: AtomicBool::new(false),\n 2692│            sessions: Arc::new(Mutex::new(HashMap::new())),\n     │...\n 2742│        loop {\n*2743│            let conn = self.listener.accept()?;\n 2744│            // C1 graceful stop: [`Broker::stop`] raises the latch then dials the\n 2745│            // throwaway connection that woke this accept — observe it and return so the\n*2746│            // serve thread (and, with it, the listener) tears down instead of leaking.\n 2747│            if self.stop.load(Ordering::Acquire) {\n 2748│                return Ok(());\n 2749│            }\n     │...\n 2758│    /// dial one throwaway connection so the blocked `accept()` wakes, observes the\n*2759│    /// latch, and returns (dropping the listener). Best-effort + idempotent — a second\n 2760│    /// call, or a call after the loop already exited, just fails the dial harmlessly.\n 2761│    /// Fills the honest gap that `serve` is otherwise unstoppable (the leaked-serve\n 2762│    /// load that starved the seed stop barrier — REQ-HAZARD-DAEMON-STOP-BARRIER).\n     │...\n 2771│    pub fn serve_once(self: &Arc<Self>) -> io::Result<JoinHandle<()>> {\n*2772│        let conn = self.listener.accept()?;\n 2773│        let me = Arc::clone(self);\n 2774│        Ok(thread::spawn(move || {\n 2775│            let _ = me.handle_conn(conn);\n     │...\n 2820│        );\n*2821│        let Ok(listener) = LocalSocketTransport::bind(&name) else {\n 2822│            return false;\n 2823│        };\n 2824│        let Ok(_client) = LocalSocketTransport::connect(&name) else {\n     │...\n 2826│        };\n*2827│        let Ok(server) = listener.accept() else {\n 2828│            return false;\n 2829│        };\n 2830│        let (_recv, send_half) = server.split();\n     │...\n 3669│    /// The endpoint-keyed counterpart of [`dispatch_input`] — a CLI `spt send` to an\n*3670│    /// spt-hosted endpoint (broker holds its PTY, no `api listen` relay) lands here.\n 3671│    /// Resolution is atomic under the sessions lock (no TOCTOU); the bytes are a\n 3672│    /// pre-rendered `<EVENT>` envelope (the caller owns the wire format — ADR-0020).\n 3673│    /// Replies [`KIND_ENDPOINT_INJECTED`] with `delivered` so the caller knows\n     │...\n 5449│\n*5450│    /// A connected local-socket pair for the unit: bind a listener, connect a\n 5451│    /// client `Stream`, accept the server `Stream`. Returns the SERVER send half\n 5452│    /// wrapped as a `SharedSend` (the broker side a `become_controller` writes\n 5453│    /// through) plus the CLIENT `Stream` (the brain side we read the wire off) and\n     │...\n 5465│        );\n*5466│        let listener = LocalSocketTransport::bind(&name).expect(\"bind listener\");\n 5467│        let client = LocalSocketTransport::connect(&name).expect(\"connect client\");\n*5468│        let server = listener.accept().expect(\"accept server side\");\n 5469│        let (recv, send) = server.split();\n 5470│        (\n 5471│            Arc::new(BrokerConn::new(send, brain_write_deadline())),\n## conn.rs\n 696│        );\n*697│        let listener = LocalSocketTransport::bind(&name).expect(\"bind\");\n 698│        let client = LocalSocketTransport::connect(&name).expect(\"connect\");\n*699│        let server = listener.accept().expect(\"accept\");\n 700│        let (_recv, send) = server.split();\n 701│        (\n 702│            BrokerConn::new(send, Duration::from_millis(2000)),\n## daemon.rs\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## deadline.rs\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## digesthub.rs\n 175│pub fn serve_digest_control(name: &str, hub: Arc<DigestHub>) -> io::Result<()> {\n*176│    let listener = LocalSocketTransport::bind(name)?;\n 177│    loop {\n*178│        let conn = listener.accept()?;\n 179│        let hub = Arc::clone(&hub);\n 180│        thread::spawn(move || {\n 181│            let _ = handle_conn(conn, &hub);\n## docshost.rs\n 216│\n*217│/// Bind the docs listener(s): `127.0.0.1:port` is REQUIRED (a failure is the\n 218│/// caller's loud log); `[::1]:port` is best-effort where the stack offers it.\n 219│/// Returns the bound v4 address (the resolved-port source for tests binding\n 220│/// port 0).\n*221│async fn bind_loopback(port: u16) -> io::Result<(tokio::net::TcpListener, Option<tokio::net::TcpListener>)> {\n*222│    let v4 = tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port))).await?;\n 223│    let actual = v4.local_addr()?.port();\n*224│    let v6 = tokio::net::TcpListener::bind((\"::1\", actual)).await.ok();\n 225│    Ok((v4, v6))\n 226│}\n 227│\n*228│async fn accept_loop(listener: tokio::net::TcpListener, docs_root: PathBuf) {\n 229│    loop {\n*230│        let Ok((stream, _peer)) = listener.accept().await else {\n 231│            continue;\n 232│        };\n 233│        let io = hyper_util::rt::TokioIo::new(stream);\n## drivehub.rs\n 181│pub fn serve_drive_control(name: &str, hub: Arc<DriveHub>) -> io::Result<()> {\n*182│    let listener = LocalSocketTransport::bind(name)?;\n 183│    loop {\n*184│        let conn = listener.accept()?;\n 185│        let hub = Arc::clone(&hub);\n 186│        thread::spawn(move || {\n 187│            let _ = handle_conn(conn, &hub);\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 28│\n*29│/// The seed-control listener name (B6): the channel `api seed`/`api listen`\n 30│/// connect to for the in-memory seed map. It is also the daemon's liveness\n 31│/// anchor — present for the daemon's whole lifetime.\n 32│pub fn seed_socket_name() -> String {\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 40│\n*41│/// The digest-control listener name (B8): the channel `spt digest <id>` connects\n 42│/// to for a snapshot pull or a delta-stream subscribe (ADR-0008 / REQ-TERM-4).\n 43│pub fn digest_socket_name() -> String {\n 44│    format!(\"spt-daemon-digest-{}.sock\", home_tag())\n   │...\n 46│\n*47│/// The drive-control listener name (M11-W2): the channel `spt shell drive` (owner\n 48│/// write) and `api drive-poll --link` (shell take-and-clear) connect to for the\n 49│/// per-shell ephemeral drive slot (REQ-SHELL-3). Broker-served like the digest\n 50│/// channel; the slot is in-memory and dies with the daemon.\n   │...\n 55│\n*56│/// The tunnel-control listener name (M11-W3): the channel `spt shell tunnel` (owner\n 57│/// duplex) and the shell binary's `api tunnel` connect to for the per-shell opaque\n 58│/// byte tunnel registry (REQ-SHELL-4). Broker-served like the drive channel; the\n 59│/// registry holds only held stream ids + the link-token stamp and dies with the\n## harnesshost.rs\n 5│//! launcher runs the adapter's `[session.self]` command and the harness binds its\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## inject.rs\n   4│//! PTY is broker-owned, it is online + broker-PTY-controllable) with NO `api\n  *5│//! listen` relay (no advertised TCP address). These are exactly the endpoints a\n   6│//! plain delivery would otherwise SILENTLY SPOOL — `deliver_tcp` finds no address\n   7│//! — so they must instead be reached by injecting the `<EVENT>` into the\n   8│//! broker-held PTY through the live translation binary.\n    │...\n  29│/// Whether `target` is an spt-hosted endpoint whose PTY the broker holds, with NO\n *30│/// `api listen` relay: it is online, has no resolvable TCP relay address, and is\n  31│/// broker-PTY-`controllable`. A harness-hosted endpoint with a relay\n  32│/// (`resolve_address` is `Some`) takes the normal TCP path; an offline or\n  33│/// non-controllable perch is not a PTY-inject target. Factored verbatim from\n    │...\n  41│    if spt_store::registry::resolve_address(target, owlery).is_some() {\n *42│        return false; // a relay listener exists → normal TCP delivery\n  43│    }\n  44│    spt_store::info::read_info(&spt_store::perch::resolve_perch_path(\n  45│        target,\n    │...\n 188│        info::write_info(&perch_path, &rec).unwrap();\n*189│        std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 190│        info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 191│        perch_path\n 192│    }\n    │...\n 210│\n*211│            // A relay listener registered → normal TCP delivery, NOT inject.\n 212│            let addr: std::net::SocketAddr = \"127.0.0.1:6553\".parse().unwrap();\n 213│            spt_store::registry::register_address(id, &addr, &owlery).unwrap();\n 214│            assert!(\n    │...\n 286│                !has_parked_idle_spool(id, &owlery),\n*287│                \"a relay endpoint delivers via its own listener, not the belt\"\n 288│            );\n 289│        });\n 290│    }\n## lib.rs\n  7│//!   live consumer would lose continuity on if the logic restarted — PTY\n *8│//!   masters (from [`spt_term`]), spawned harness children, accepted/listening\n  9│//!   sockets — and runs no logic;\n 10│//! - a **brain** (restartable logic) holds routing, registry, manifest parse,\n 11│//!   and the re-hosted [`spt_live`] lifecycle, attaching to the broker over a\n   │...\n 28│//! **B3 (this commit)** consolidates the interim no-daemon model into the brain:\n*29│//! the `api listen` pulse / Psyche / drop-ingest loops become scheduled\n 30│//! in-process loops ([`lifecycle::BrainLifecycle`]) paced by a durable,\n 31│//! configurable period ([`config::DaemonConfig`]) instead of the interim 5s\n 32│//! constant; the only residue left in the harness-hosted session is the thin,\n## lifecycle.rs\n    1│//! The brain's LiveAgent lifecycle host (B3) — the consolidated re-host of the\n   *2│//! interim `api listen` runner's live half.\n    3│//!\n   *4│//! In the no-daemon interim model the long-lived `spt api listen` process hosted\n    5│//! the LiveAgent lifecycle (per-event psyche turns, pulse-driven drop ingest, echo\n    6│//! fires, graceful signoff) directly in the agent's session. Consolidation\n    7│//! (REQ-DAEMON-1) moves that orchestration **down into `spt-daemon`** so the\n     │...\n 1268│            || {\n*1269│                let ready = perch::resolve_ready_file(&self.id, ParentHint::Infer);\n 1270│                if let Err(e) = std::fs::remove_file(&ready) {\n 1271│                    if e.kind() != std::io::ErrorKind::NotFound {\n 1272│                        teardown_err = Some(e.to_string());\n     │...\n 1328│        // 3.2 — sweep a stale signoff sentinel before supervising, exactly as a\n*1329│        // listener start does, so a prior crash's sentinel can't self-trigger.\n 1330│        self.sweep_on_start();\n 1331│        while !stop.load(Ordering::Relaxed) {\n 1332│            if !is_process_alive(owner_pid) {\n     │...\n 1415│        write_info(&perch_path, &info).unwrap();\n*1416│        let ready = perch::resolve_ready_file(id, ParentHint::Infer);\n 1417│        if let Some(p) = ready.parent() {\n 1418│            std::fs::create_dir_all(p).unwrap();\n 1419│        }\n     │...\n 2508│\n*2509│            let ready = perch::resolve_ready_file(\"doyle\", ParentHint::Infer);\n 2510│            std::fs::create_dir_all(ready.parent().unwrap()).unwrap();\n 2511│            std::fs::write(&ready, \"\").unwrap();\n 2512│\n     │...\n 2540│\n*2541│            let ready = perch::resolve_ready_file(\"doyle\", ParentHint::Infer);\n 2542│            std::fs::create_dir_all(ready.parent().unwrap()).unwrap();\n 2543│            std::fs::write(&ready, \"\").unwrap();\n 2544│\n     │...\n 2610│            seed_supervised_perch(\"doyle\", DEAD_PID);\n*2611│            let ready = perch::resolve_ready_file(\"doyle\", ParentHint::Infer);\n 2612│            assert!(ready.exists(), \"precondition: ready marker present\");\n 2613│\n 2614│            let stop = AtomicBool::new(false);\n     │...\n 2640│            seed_supervised_perch(\"doyle\", std::process::id());\n*2641│            let ready = perch::resolve_ready_file(\"doyle\", ParentHint::Infer);\n 2642│\n 2643│            // Owner is this (alive) process; stop is already set → loop exits with\n 2644│            // no signoff and leaves the perch untouched.\n## linkhost.rs\n 211│\n*212│/// At shell-online (the `api bind-shell` seam), open the shell's opaque tunnel if\n 213│/// its manifest enables it (M11-W3, REQ-SHELL-4). Resolves the shell's `[shell]`\n 214│/// body and, when `[shell.tunnel].enable`, ensures the broker-held loopback stream\n 215│/// pair under the CURRENT link token — so the tunnel's lifetime is the link's\n## livehost.rs\n   15│//!\n  *16│//! **Online-only is staged behind the interim.** The interim `spt api listen`\n   17│//! process still first-hosts its live agents today; those perches are\n   18│//! `status`-ABSENT (not \"online\"), so this scan ignores them — no double-host in\n   19│//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the\n  *20│//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this\n   21│//! reconcile reacts within one tick, the same way the shell waker reacts to a\n   22│//! CLI-process status flip. The full bind→online→brain-restart→re-host arc is\n   23│//! proven through the real brain process by the W0.4 production int.\n     │...\n  141│///\n *142│/// `status` is daemon-authoritative (CONTEXT:194); interim `listen` perches are\n  143│/// `status`-ABSENT so they are never hosted here (no double-host) until W0.2.\n  144│// [impl->REQ-DAEMON-1]\n  145│pub fn reconcile_once(\n     │...\n  658│/// (spt-hosted, broker-PTY) perches are reconciled here. A HARNESS-HOSTED relay\n *659│/// live agent (`api listen`, `controllable == Some(false)`) is legitimately online\n  660│/// with NO broker session (its liveness is the relay + ready marker), so it is\n  661│/// NEVER marked offline by this pass. A `controllable == None` perch (legacy /\n  662│/// dead-psyche like wall-a) is also exempt here — its liveness/phantom class is\n     │...\n  887│    let _ = std::fs::remove_file(psyche_perch.join(\"info.json\"));\n *888│    let _ = std::fs::remove_file(perch::resolve_ready_file(&psyche_id, ParentHint::Explicit(id)));\n  889│    eprintln!(\n  890│        \"LEGACY_PSYCHE_SWEEP_REAP:{id} pid={pid}: reaped stranded pre-W3 resident psyche \\\n  891│         + cleared its stale `{psyche_id}` ready registration\"\n     │...\n 2067│    #[test]\n*2068│    fn reconcile_attempts_resume_for_a_registered_woken_endpoint() {\n 2069│        with_home(|_| {\n 2070│            let drops = perch::spt_home().join(\"drops\");\n 2071│            std::fs::create_dir_all(&drops).unwrap();\n## nethost.rs\n 811│    /// per key — the ALPN is the pre-trust/trusted boundary (ADR-0005).\n*812│    /// The meet-rotation listener loop rides the same runtime.\n 813│    // [impl->REQ-DAEMON-2]\n 814│    // [impl->REQ-SUBNET-2]\n 815│    pub fn start(cfg: NetConfig) -> io::Result<NetHost> {\n## notif.rs\n 313│/// REQ-NOTIF-2's M5 generalization): render a surfaced notif through every\n*314│/// registered shell adapter that (a) has an instance attached to `endpoint`\n 315│/// and (b) declares the template. Entirely manifest-driven — spt-core never\n 316│/// names a capability or knows what \"render\" means; the adapter's template is\n 317│/// the whole contract (`spt notify` → presence MRA → the winning node spawns\n    │...\n 696│                \"ling\",\n*697│                spt_net::net::registry::Instance {\n 698│                    node: \"remote-hex\".into(),\n 699│                    status: spt_net::net::registry::Status::Active,\n 700│                    epoch: 1,\n## pairhost.rs\n  10│//!   subnet-global [`PairingRateLimiter`] (red-team #11) is the standing-\n *11│//!   listener guard; [`respond`] additionally sheds concurrent ceremonies at\n  12│//!   the door (`try_lock`) and bounds each admitted ceremony to the\n  13│//!   limiter's own [`CEREMONY_TIMEOUT_SECS`] so a stalling peer cannot wedge\n  14│//!   the responder (the limiter's stale-slot reclaim is the backstop).\n  15│//! - **Meet rotation** ([`spawn_meet_rotation`]): for each member subnet, a\n *16│//!   short-lived rendezvous listener bound to the token-derived identity\n  17│//!   (`pairing::meet`), re-bound every TOTP step — how a joiner that knows\n  18│//!   nothing but (name, code) finds this responder over LAN or relay.\n  19│//! - **Joiner leg** ([`join_ceremony`]): dial via rendezvous, run the\n    │...\n 157│/// The meet-rotation loop: for every member subnet, keep a rendezvous meet\n*158│/// listener bound under the (name, current-step)-derived identity, answering\n 159│/// joiner probes with `real_addr` — the responder's actual pairing-endpoint\n 160│/// address. Re-binds at each TOTP step boundary (window rotation = re-bind,\n 161│/// `pairing::meet` docs); re-reads the subnet store per window so a freshly\n    │...\n 180│    handle.spawn(async move {\n*181│        // The PREVIOUS window's listeners, kept serving one extra period\n*182│        // (M7 acceptance fix): published records outlive listeners by the\n 183│        // DNS TTL, so a joiner resolving the step-1 token must find a live\n*184│        // listener there, not a ghost. Overlap = every published id is\n 185│        // dialable for its full discoverable life.\n 186│        let mut prev_tasks: Vec<tokio::task::JoinHandle<()>> = Vec::new();\n 187│        loop {\n    │...\n 189│            // Serve-state filter (REQ-SUBNET-5): a detached subnet's meet\n*190│            // listener does not rotate — a joiner cannot rendezvous with a\n 191│            // membership this node deliberately is not serving. Re-read per\n 192│            // window like the subnet store itself.\n 193│            let attachment = spt_store::attachment::AttachmentStore::load();\n    │...\n 199│                .collect();\n*200│            // One listener task per subnet for this window's lifetime.\n 201│            let mut window_tasks = Vec::new();\n 202│            for name in names {\n 203│                let relay = relay.clone();\n    │...\n 205│                window_tasks.push(tokio::spawn(async move {\n*206│                    let listener = match bind_rendezvous(&name, step, relay, discovery, scope).await\n 207│                    {\n 208│                        Ok(l) => l,\n 209│                        Err(e) => {\n    │...\n 214│                    // Window diagnostics (live-debugging the meet path needs\n*215│                    // the derived id + what this listener actually advertises\n*216│                    // — an undiscoverable listener is indistinguishable from\n 217│                    // a dead one without this line).\n 218│                    eprintln!(\n 219│                        \"PAIR_MEET_UP:{name} step={step} id={} addr={:?}\",\n*220│                        listener.endpoint().node_id_hex(),\n*221│                        listener.endpoint().addr()\n 222│                    );\n 223│                    // Serve probes until the window task is aborted at the\n 224│                    // step boundary; each failure only ends one exchange.\n 225│                    loop {\n*226│                        match listener.serve_once(&real_addr).await {\n 227│                            Ok(()) => eprintln!(\"PAIR_MEET_ANSWERED:{name} step={step}\"),\n 228│                            Err(e) => {\n 229│                                eprintln!(\"PAIR_MEET_SERVE_ERR:{name}: {e}\");\n    │...\n 257│pub enum JoinFail {\n*258│    /// No meet listener answered for any window token — no seed-holder\n 259│    /// reachable under that subnet name right now.\n 260│    NoSeedHolder(String),\n 261│    /// The ceremony ran and the confirmation failed — in practice: wrong code."},"isError":false,"timestamp":1784197385424}}
{"type":"message","id":"5e3debfc","parentId":"4106aae8","timestamp":"2026-07-16T10:23:05.440Z","message":{"role":"toolResult","toolCallId":"call_xxWmGOGi2vrcM7nWN8RECmd4|fc_017bff223290b7ba016a58b1095d18819ab4508996460c3405","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-daemon/\n### Cargo.toml\n 52|\n*53|# De-elevated daemon spawn (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN):\n 54|# the sudo-invoker drop (geteuid/setuid/setgroups/getpwuid_r). Matches the\n 55|# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half\n 56|# uses raw advapi32 externs (the daemon.rs detached_no_inherit posture).\n\n### src/\n#### brainproc.rs\n 155|///\n*156|/// It inherits the broker's already-unelevated token as a plain child, so it\n*157|/// runs no de-elevation guard of its own.\n 158|// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 159|pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n 160|    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n#### daemon.rs\n 57|        crate::conn::init_log_anchor();\n*58|        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n*59|        // `spt daemon` (sudo / elevated shell) must not serve with the\n*60|        // elevated token — its pipes would deny unelevated clients (Windows)\n 61|        // or root the user's state universe (Linux). Unix drops privileges\n 62|        // in-process before any state is touched; Windows cannot drop a\n*63|        // token in-place, so it respawns de-elevated via the UAC linked\n*64|        // token and exits (the child re-enters here unelevated). With no\n*65|        // unelevated identity to drop to, serving continues as-is, loudly —\n 66|        // a consistent universe, never a torn one.\n*67|        // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n 68|        // Drop target: the ELECTED default user when one exists, else the\n 69|        // sudo invoker — electing it on first use (M8 decision 8,\n 70|        // REQ-INSTALL-6: the first `sudo spt` decides; thereafter every\n*71|        // elevated launch lands in the same account's universe).\n 72|        // [impl->REQ-INSTALL-6]\n 73|        #[cfg(unix)]\n 74|        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n 75|            match invoker.drop_in_process() {\n*76|                Ok(()) => eprintln!(\"DEELEVATED: daemon dropped to uid {}\", invoker.uid),\n 77|                // A drop target existed but the drop failed: serving as\n 78|                // root would root the user's state universe — a known-torn\n 79|                // state. ABORT (user-ratified 2026-06-06: refuse, don't limp).\n...\n 82|                        \"DEELEVATE_FAIL: {e} — refusing to serve as root over a user universe; \\\n*83|                         run spt unelevated\"\n 84|                    )));\n 85|                }\n 86|            }\n...\n 88|        #[cfg(windows)]\n*89|        if crate::deelevate::is_elevated() {\n 90|            let exe = std::env::current_exe()?;\n*91|            match crate::deelevate::spawn_deelevated(\n 92|                &exe.to_string_lossy(),\n 93|                // --detached BELT (REQ-HAZARD-DETACHED-DAEMON-STDIO): the respawned\n*94|                // unelevated daemon then runs detach_console + the null-handles guard,\n 95|                // so it never keeps live inherited stdio (matches every other rung).\n 96|                &[\"daemon\".to_string(), \"run\".to_string(), \"--detached\".to_string()],\n 97|            ) {\n...\n 99|                    eprintln!(\n*100|                        \"DEELEVATED_RESPAWN: unelevated daemon pid {pid}; elevated copy exits\"\n 101|                    );\n 102|                    return Ok(());\n 103|                }\n*104|                // Uniformly-elevated universe (no unelevated identity):\n*105|                // serving elevated is consistent there. Loud anyway.\n 106|                Ok(None) => eprintln!(\n*107|                    \"ELEVATED_DAEMON: no unelevated identity to drop to — serving with the \\\n*108|                     elevated token\"\n 109|                ),\n*110|                // Target existed, respawn failed: an elevated daemon is a\n 111|                // known-torn state. ABORT (user-ratified 2026-06-06).\n 112|                Err(e) => {\n 113|                    return Err(io::Error::other(format!(\n*114|                        \"DEELEVATE_FAIL: {e} — refusing to serve with the elevated token; run \\\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...\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*49|    /// Not elevated (or no de-elevation target exists): spawn as-is.\n 50|    Inherit,\n*51|    /// Elevated with an unelevated identity available: spawn de-elevated.\n 52|    DeElevate,\n 53|}\n 54|\n*55|/// The pure decision: de-elevate iff the process is elevated AND an\n*56|/// unelevated identity exists to drop to (Windows: the UAC linked token;\n*57|/// Linux: a parsed `SUDO_UID`). Elevated-without-target spawns `Inherit`\n*58|/// — a consistent elevated universe with a loud warning beats a torn one.\n#### pairhost.rs\n 116|            // live ceremony connection. A completed re-pair of a tombstoned key\n*117|            // clears the tombstone (decision 10) — the deliberate elevated act.\n 118|            // [impl->REQ-MESH-2]\n 119|            roster.clear_tombstone(&outcome.subnet, &outcome.joiner_hex);\n 120|            record_roster_peer(\n#### service.rs\n 81|/// `daemon run` is foreground by contract (REQ-DAEMON-7). It refuses ONLY when\n*82|/// it would otherwise vanish: on Windows, when an elevated invocation has a\n*83|/// real de-elevation target (it would respawn detached/de-elevated, KH 5.7).\n*84|/// A uniformly-elevated or headless Windows session has NO target — there `run`\n*85|/// serves elevated in the foreground consistently (and must, e.g. an elevated\n 86|/// self-hosted CI runner with no desktop shell). Unix always serves foreground\n 87|/// (privileges drop in-process). So the refuse is gated on \"would vanish\", not\n*88|/// on elevation alone.\n 89|// [impl->REQ-DAEMON-7]\n 90|pub fn run_refused_when_vanishing(is_windows: bool, would_vanish: bool) -> bool {\n 91|    is_windows && would_vanish\n...\n 389|    // [unit->REQ-DAEMON-7] `run` refuses only when it WOULD VANISH: on Windows\n*390|    // with a real de-elevation target. A headless/uniformly-elevated Windows\n 391|    // session (no target → would_vanish=false) serves foreground, as does unix.\n 392|    #[test]\n 393|    fn run_refused_only_when_windows_and_vanishing() {\n\n### tests/\n#### brain_swap.rs\n 18|//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n*19|//! Windows' installer-detection heuristic (UAC) forces elevation for any\n 20|//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n 21|//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n*22|//! elevation). Do not rename this to match the module under test.\n 23|//!\n 24|//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n 25|//! *in-process* brain-only swap shape (the engine's `apply_brain_only` over an\n\n## spt-msg/src/\n### lib.rs\n 10|//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the\n*11|//! mis-elevated legacy reply-to framing). The delivery routing, listener\n 12|//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against\n 13|//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).\n 14|//!\n### wire.rs\n 3|//! The frame carries `(from, body)` **structurally** — there is no\n*4|//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated\n 5|//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in\n 6|//! [`spt_proto::event`], composed once at the delivery edge, never on the wire).\n 7|//! spt-core mandates **no** byte-for-byte TCP interop with any sister `spt`.\n\n## spt-store/src/\n### roster.rs\n 37|//! Cross-node convergence of an *un*-tombstone is **not** D3's concern.\n*38|//! [`clear_tombstone`] is a *local* elevated act (the re-pair). The compromised\n 39|//! path re-pairs under a **fresh, never-tombstoned key** (the milestone's\n 40|//! decision 10), so the old tombstone correctly stands forever; the benign\n 41|//! N-1 re-seed + timeboxed rotation propagation is **Mesh-D7**. D3 tests\n...\n 286|    /// **Clear-on-repair**: drop the tombstone for `(subnet, pubkey)` — the\n*287|    /// deliberate elevated act a completed re-pair performs. Returns whether one\n 288|    /// was cleared. (A local op; see the module's convergence-boundary note.)\n 289|    // [impl->REQ-MESH-2]\n 290|    pub fn clear_tombstone(&mut self, subnet: &str, pubkey_hex: &str) -> bool {\n\n## spt/\n### Cargo.toml\n 32|# update` command. NOTE: the bin name + the test name deliberately avoid the\n*33|# substring \"update\" — Windows' installer-detection heuristic forces UAC elevation\n 34|# on a manifest-less exe whose name contains it (os error 740 in CI).\n 35|[[bin]]\n 36|name = \"post_step_fixture\"\n...\n 105|\n*106|# OS privilege-elevation probe for the node-bound code fetch (REQ-PAIR-6). Only the\n 107|# binary pulls the OS deps; the core stays dep-minimal. Matches the workspace's existing\n 108|# libc/windows-sys usage (spt-msg, spt-store) rather than the heavyweight `windows`.\n 109|[target.'cfg(unix)'.dependencies]\n...\n 122|    \"Win32_System_Threading\",\n*123|    # REQ-ELEVATE-1: ShellExecuteW(\"runas\") launches the UAC-elevated console;\n*124|    # GetConsoleProcessList lets the elevated child detect a fresh (sole-owner)\n 125|    # console so it pauses before exit instead of vanishing.\n 126|    \"Win32_UI_Shell\",\n 127|    \"Win32_UI_WindowsAndMessaging\",\n\n### src/\n#### cli.rs\n 30|\n*31|use crate::elevation::{self, Elevation, Gate};\n 32|use crate::roster;\n 33|\n 34|/// Product tag embedded in the `otpauth://` provisioning URI (the authenticator-app\n...\n 37|\n*38|/// `spt subnet show-code`/`create` refused the fetch for lack of OS elevation. Distinct\n 39|/// from the crate-wide codes (`0` ok · `1` runtime refusal · `2` usage): `3` says the\n*40|/// fetch was *refused, not failed* — re-run elevated, or read the code from your\n 41|/// authenticator app.\n*42|const EXIT_NOT_ELEVATED: i32 = 3;\n 43|\n 44|#[derive(Parser)]\n 45|#[command(\n...\n 1047|    /// code, the `otpauth://` provisioning URI, and a terminal QR of it.\n*1048|    /// Gated behind OS elevation (the seed-reveal path).\n 1049|    Create {\n 1050|        /// The new subnet's name.\n 1051|        name: String,\n...\n 1056|    /// `create` — current code, `otpauth://` URI, terminal QR, expiry.\n*1057|    /// Gated behind OS elevation (or read the code from your authenticator\n 1058|    /// app). With no name the node's sole subnet is used; if it holds\n 1059|    /// several, the name is required (never guessed).\n 1060|    ShowCode {\n...\n 1068|    /// name and code when omitted (interactive terminals). Gated behind\n*1069|    /// OS elevation — joining enrolls this whole machine.\n 1070|    Join {\n 1071|        /// The subnet to join (as named on the member machine).\n 1072|        name: Option<String>,\n...\n 1085|    /// Removes the subnet's seed, its trust rows, its serve-state, and its\n*1086|    /// registry snapshot here. Gated behind OS elevation (membership exit\n 1087|    /// destroys trust material). The remaining members still hold the old\n 1088|    /// seed — rotate it there if this machine should not rejoin.\n 1089|    Leave {\n...\n 1097|    /// tick. Takes a full pubkey hex, an unambiguous prefix, or a node\n*1098|    /// label. Gated behind OS elevation (trust mutation).\n 1099|    Prune {\n 1100|        /// The dead identity: pubkey hex, unambiguous prefix, or label.\n 1101|        node: String,\n...\n 1111|    /// locked out and must re-pair. Each target is a pubkey hex, an\n*1112|    /// unambiguous prefix, or a label. Gated behind OS elevation.\n 1113|    Revoke {\n 1114|        /// The identities to revoke: pubkey hex, unambiguous prefix, or label.\n 1115|        #[arg(required = true)]\n...\n 3870|/// Run the per-machine daemon (`spt daemon run`, M8 decision 5). FOREGROUND +\n*3871|/// unelevated by contract (REQ-DAEMON-7): this process IS the daemon, blocking\n 3872|/// the broker + seed-control channels until signalled. Never auto-detaches —\n*3873|/// the background/de-elevated path lives in `start`.\n 3874|///\n 3875|/// Two guards before binding:\n*3876|/// - Windows + elevated ⇒ REFUSE (exit 2). An elevated `run` would otherwise\n*3877|///   respawn detached/de-elevated (KH 5.7) and vanish into the background.\n 3878|/// - A managed service is already active ⇒ warn loudly (running inline fights\n 3879|///   it for the socket — the kitsubito 2026-06-08 restart-loop).\n 3880|// [impl->REQ-DAEMON-3]\n...\n 3901|    // detached-in-fact (no console OR a piped stderr) EVEN IF `--detached` was not\n*3902|    // passed — the stale-bare-at-logon-task / bare-deelevated-respawn / any-unknown\n 3903|    // rung case. Runs AFTER detach_console (idempotent) and BEFORE any bind/output, so\n 3904|    // a filling inherited pipe can never block the daemon. A foreground human keeps\n 3905|    // their console (both checks false).\n...\n 3912|        true,\n*3913|        spt_daemon::deelevate::has_deelevation_target(),\n 3914|    ) {\n*3915|        // Only the vanishing case: elevated WITH an unelevated desktop shell to\n*3916|        // drop to (run would respawn detached/de-elevated). A headless or\n*3917|        // uniformly-elevated session has no target and serves foreground below.\n 3918|        eprintln!(\n*3919|            \"DAEMON_RUN_ELEVATED: `daemon run` is foreground; from an elevated shell it would \\\n 3920|             respawn in the background — use `spt daemon start` for a background daemon, or run \\\n#### elevation.rs\n*1|//! OS privilege-elevation gate for the node-bound pairing-code fetch (REQ-PAIR-6).\n 2|//!\n 3|//! Surfacing a subnet's live TOTP code off a trusted node is the *privileged* fetch\n*4|//! path: it hands out subnet-joining authority, so the process must be elevated\n 5|//! (Windows UAC / Linux root-or-equivalent). When it is not, we refuse the node-bound\n 6|//! fetch and point the user at their authenticator app — where the same seed was\n 7|//! provisioned at pairing (the `otpauth://` URI), giving the code without privilege.\n...\n 12|//! cannot run hermetically in CI (the test process's real euid/token is whatever the\n*13|//! runner happens to be). So the *decision* — does this elevation level show the code\n 14|//! or fall back? — is factored into [`gate_for`], a pure function unit-tested both\n*15|//! ways. An unknown/unsupported platform maps to [`Elevation::Unknown`], which\n*16|//! [`gate_for`] treats as NOT elevated: **fail safe** — never leak the code when we\n 17|//! cannot positively confirm privilege.\n 18|// [impl->REQ-PAIR-6]\n 19|\n...\n 21|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n*22|pub enum Elevation {\n*23|    /// Confirmed elevated: Windows token is elevated, or Unix euid == 0.\n*24|    Elevated,\n*25|    /// Confirmed NOT elevated.\n*26|    NotElevated,\n 27|    /// Could not determine (unsupported platform, or the OS query failed). Treated as\n*28|    /// NOT elevated by [`gate_for`] — we never assume privilege we cannot confirm.\n 29|    ///\n 30|    /// Constructed only on Windows (an `OpenProcessToken`/`GetTokenInformation`\n 31|    /// failure) and on unsupported platforms; on Unix `geteuid` cannot fail, so the\n...\n 40|pub enum Gate {\n*41|    /// Elevated — render the code + `otpauth://` URI.\n 42|    Show,\n*43|    /// Not elevated (or undeterminable) — refuse, point at the authenticator app.\n 44|    Fallback,\n 45|}\n 46|\n*47|/// The pure gate decision (the testable seam). Only a *positively confirmed* elevation\n*48|/// shows the code; `NotElevated` and `Unknown` both fall back (fail safe).\n 49|// [impl->REQ-PAIR-6]\n*50|pub fn gate_for(elevation: Elevation) -> Gate {\n*51|    match elevation {\n*52|        Elevation::Elevated => Gate::Show,\n*53|        Elevation::NotElevated | Elevation::Unknown => Gate::Fallback,\n 54|    }\n 55|}\n 56|\n...\n 61|    format!(\n*62|        \"ELEVATION_REQUIRED: showing the code for subnet '{subnet}' off this node needs an \\\n#### main.rs\n 11|mod cli;\n*12|mod elevation;\n 13|mod helpfmt;\n 14|mod picker;\n 15|mod rc;\n...\n 66|\n*67|    // The sudo de-elevation drop (KNOWN-HAZARDS 5.7) runs BEFORE anything\n 68|    // touches state: a sudo'd `spt` otherwise lives in root's universe\n 69|    // (sudo flips $HOME, so spt_home — state, node identity, the daemon\n 70|    // socket names — all resolve wrong) and roots whatever it writes. Drop\n*71|    // to the ELECTED default user (uid/gid/HOME) while keeping the elevation\n 72|    // PROOF for the gated commands (`subnet create`/`join`, show-code):\n 73|    // the user paid for the gate by sudo'ing; the drop is state safety,\n 74|    // not authz revocation. Genuine root (no SUDO_*) is left alone — root's\n...\n 78|    // account ELECTION (M8 decision 8, REQ-INSTALL-6): `daemon_target_user`\n*79|    // — not the bare `sudo_invoker` — so the first elevated `spt` records\n 80|    // `/etc/spt-core/default-user` and every later one drops deterministically\n 81|    // to that account. (The Daemon::run / spawn_detached election guards run\n 82|    // AFTER this drop — they always see euid != 0 and no-op; electing only\n...\n 85|    // persisted because this early drop shadowed both daemon-side guards.)\n*86|    // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n 87|    // [impl->REQ-INSTALL-6]\n 88|    #[cfg(unix)]\n 89|    if let Some(invoker) = spt_daemon::deelevate::daemon_target_user() {\n...\n 91|            Ok(()) => {\n*92|                // Keep the elevation PROOF for the gated commands, but stay\n 93|                // silent on the routine drop (the one-time election itself\n*94|                // announces via `ELECTED_DEFAULT_USER`): a \"DEELEVATED:\n 95|                // running as uid N\" line only confuses an end user who just\n 96|                // wanted to create/join a subnet.\n*97|                elevation::mark_proven();\n 98|            }\n 99|            // A drop target existed but the drop failed: continuing would\n 100|            // run root over the user's universe — a known-torn state.\n...\n 103|            Err(e) => {\n*104|                eprintln!(\"DEELEVATE_FAIL: {e} — refusing to run elevated over a user universe\");\n 105|                std::process::exit(1);\n 106|            }\n 107|        }\n\n### tests/\n#### composite_e2e.rs\n 13|//! NOTE: the file/binary name deliberately avoids the substring \"update\" —\n*14|//! Windows' installer-detection heuristic forces UAC elevation on a\n 15|//! manifest-less exe whose name contains it (os error 740; the\n 16|//! post_step_fixture precedent).\n 17|//\n#### oneliner_e2e.rs\n 9|//! (File is named `oneliner_e2e`, not `install_e2e`: Windows' UAC\n*10|//! installer-detection heuristic demands elevation for any executable whose\n 11|//! name contains \"install\" — the test binary inherits the file name and\n 12|//! os error 740s before it can run.)\n 13|//!\n#### resume_no_control_steal_e2e.rs\n 87|//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows installer-detection\n*88|//! demands UAC elevation for such an exe → `os error 740` at `--list` (KH win-update-exe-\n 89|//! uac740). This file is deliberately `resume_no_control_steal_e2e`.\n 90|\n 91|use std::path::PathBuf;\n#### trial_drain_drive_e2e.rs\n 96|//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows\n*97|//! installer-detection heuristics demand UAC elevation for a test exe whose name\n*98|//! contains those tokens → `os error 740` \"requires elevation\" at `--list` (KH\n 99|//! win-update-exe-uac740). This file is deliberately `trial_drain_drive_e2e`.\n 100|\n 101|use std::path::PathBuf;\n\n#### fixtures/\n##### post_step_fixture.rs\n 5|//! excludes it). NOT a production binary. The bin name avoids the substring\n*6|//! \"update\" — Windows installer-detection forces UAC elevation on a manifest-less\n 7|//! exe whose name contains it (os error 740).\n 8|//!\n 9|//! Contract (spt-core → here): one stdin JSON line = the published seam\n\n# docs/\n## KNOWN-HAZARDS.md\n 212|\n*213|### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\n*214|<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\n*215|- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill ...\n*216|- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run`...\n*217|- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n*218|- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n 219|\n 220|<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n 221|### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n 222|- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\n*223|- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\n 224|- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\n 225|- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call si...\n 226|- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\n...\n 235|### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\n*236|- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot...\n*237|- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interact...\n*238|- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n*239|- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n*240|- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n 241|- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\n 242|\n 243|### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n*244|- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-el...\n*245|- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to th...\n*246|- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one qu...\n*247|- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this compos...\n 248|- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n 249|\n 250|<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\n...\n 694|| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |\n*695|| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |\n 696|| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |\n 697|| 5.12 | Native-PTY spawn PATHEXT-resolves a bare program + wraps non-PE targets (.cmd/.bat→`cmd /c`, .ps1→powershell), bypassing portable-pty's shim-first `which` (CreateProcessW os error 193) | `spt_term::winprog` → `PtySession::spawn_program_in` |\n 698|| 6.1 | Single path/registry source of truth | storage layout |\n...\n 713|| 7.9 | A daemon-state wire change (e.g. the v0.9.0 agnostic Seed) needs a deliberate broker restart — the resident old broker can't read new bytes; the seed-skew EOF surfaces an actionable `spt daemon stop` hint, not \"failed to fill whole buffer\"; forward: additive + serde-default daemon-state | `cmd_seed`/`seed_fail_message`, broker seed-control residency |\n*714|| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job...\n 715|| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |\n 716|| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |\n 717|| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |\n\nShowing files 1-20 of 28+. Use skip=20 for the next page, or narrow paths/pattern.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"crates, docs/KNOWN-HAZARDS.md, docs/adr, CONTEXT.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":133,"fileCount":20,"files":["crates/spt-daemon/Cargo.toml","crates/spt-daemon/src/brainproc.rs","crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deelevate.rs","crates/spt-daemon/src/pairhost.rs","crates/spt-daemon/src/service.rs","crates/spt-daemon/tests/brain_swap.rs","crates/spt-msg/src/lib.rs","crates/spt-msg/src/wire.rs","crates/spt-store/src/roster.rs","crates/spt/Cargo.toml","crates/spt/src/cli.rs","crates/spt/src/elevation.rs","crates/spt/src/main.rs","crates/spt/tests/composite_e2e.rs","crates/spt/tests/fixtures/post_step_fixture.rs","crates/spt/tests/oneliner_e2e.rs","crates/spt/tests/resume_no_control_steal_e2e.rs","crates/spt/tests/trial_drain_drive_e2e.rs","docs/KNOWN-HAZARDS.md"],"fileMatches":[{"path":"crates/spt-daemon/Cargo.toml","count":1},{"path":"crates/spt-daemon/src/brainproc.rs","count":2},{"path":"crates/spt-daemon/src/daemon.rs","count":20},{"path":"crates/spt-daemon/src/deelevate.rs","count":20},{"path":"crates/spt-daemon/src/pairhost.rs","count":1},{"path":"crates/spt-daemon/src/service.rs","count":6},{"path":"crates/spt-daemon/tests/brain_swap.rs","count":2},{"path":"crates/spt-msg/src/lib.rs","count":1},{"path":"crates/spt-msg/src/wire.rs","count":1},{"path":"crates/spt-store/src/roster.rs","count":2},{"path":"crates/spt/Cargo.toml","count":4},{"path":"crates/spt/src/cli.rs","count":20},{"path":"crates/spt/src/elevation.rs","count":20},{"path":"crates/spt/src/main.rs","count":9},{"path":"crates/spt/tests/composite_e2e.rs","count":1},{"path":"crates/spt/tests/fixtures/post_step_fixture.rs","count":1},{"path":"crates/spt/tests/oneliner_e2e.rs","count":1},{"path":"crates/spt/tests/resume_no_control_steal_e2e.rs","count":1},{"path":"crates/spt/tests/trial_drain_drive_e2e.rs","count":2},{"path":"docs/KNOWN-HAZARDS.md","count":18}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt-daemon/\n### Cargo.toml\n 52│\n*53│# De-elevated daemon spawn (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN):\n 54│# the sudo-invoker drop (geteuid/setuid/setgroups/getpwuid_r). Matches the\n 55│# workspace's existing libc usage (spt, spt-msg, spt-store); the Windows half\n 56│# uses raw advapi32 externs (the daemon.rs detached_no_inherit posture).\n\n### src/\n#### brainproc.rs\n 155│///\n*156│/// It inherits the broker's already-unelevated token as a plain child, so it\n*157│/// runs no de-elevation guard of its own.\n 158│// [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]\n 159│pub fn run_brain(generation: u64, reason: StartReason) -> io::Result<()> {\n 160│    // Persist this brain generation's stderr to the rotating SPT_HOME/logs sink\n#### daemon.rs\n  57│        crate::conn::init_log_anchor();\n *58│        // Entry elevation guard (KNOWN-HAZARDS 5.7): a DIRECTLY elevated\n *59│        // `spt daemon` (sudo / elevated shell) must not serve with the\n *60│        // elevated token — its pipes would deny unelevated clients (Windows)\n  61│        // or root the user's state universe (Linux). Unix drops privileges\n  62│        // in-process before any state is touched; Windows cannot drop a\n *63│        // token in-place, so it respawns de-elevated via the UAC linked\n *64│        // token and exits (the child re-enters here unelevated). With no\n *65│        // unelevated identity to drop to, serving continues as-is, loudly —\n  66│        // a consistent universe, never a torn one.\n *67│        // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n  68│        // Drop target: the ELECTED default user when one exists, else the\n  69│        // sudo invoker — electing it on first use (M8 decision 8,\n  70│        // REQ-INSTALL-6: the first `sudo spt` decides; thereafter every\n *71│        // elevated launch lands in the same account's universe).\n  72│        // [impl->REQ-INSTALL-6]\n  73│        #[cfg(unix)]\n  74│        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n  75│            match invoker.drop_in_process() {\n *76│                Ok(()) => eprintln!(\"DEELEVATED: daemon dropped to uid {}\", invoker.uid),\n  77│                // A drop target existed but the drop failed: serving as\n  78│                // root would root the user's state universe — a known-torn\n  79│                // state. ABORT (user-ratified 2026-06-06: refuse, don't limp).\n    │...\n  82│                        \"DEELEVATE_FAIL: {e} — refusing to serve as root over a user universe; \\\n *83│                         run spt unelevated\"\n  84│                    )));\n  85│                }\n  86│            }\n    │...\n  88│        #[cfg(windows)]\n *89│        if crate::deelevate::is_elevated() {\n  90│            let exe = std::env::current_exe()?;\n *91│            match crate::deelevate::spawn_deelevated(\n  92│                &exe.to_string_lossy(),\n  93│                // --detached BELT (REQ-HAZARD-DETACHED-DAEMON-STDIO): the respawned\n *94│                // unelevated daemon then runs detach_console + the null-handles guard,\n  95│                // so it never keeps live inherited stdio (matches every other rung).\n  96│                &[\"daemon\".to_string(), \"run\".to_string(), \"--detached\".to_string()],\n  97│            ) {\n    │...\n  99│                    eprintln!(\n*100│                        \"DEELEVATED_RESPAWN: unelevated daemon pid {pid}; elevated copy exits\"\n 101│                    );\n 102│                    return Ok(());\n 103│                }\n*104│                // Uniformly-elevated universe (no unelevated identity):\n*105│                // serving elevated is consistent there. Loud anyway.\n 106│                Ok(None) => eprintln!(\n*107│                    \"ELEVATED_DAEMON: no unelevated identity to drop to — serving with the \\\n*108│                     elevated token\"\n 109│                ),\n*110│                // Target existed, respawn failed: an elevated daemon is a\n 111│                // known-torn state. ABORT (user-ratified 2026-06-06).\n 112│                Err(e) => {\n 113│                    return Err(io::Error::other(format!(\n*114│                        \"DEELEVATE_FAIL: {e} — refusing to serve with the elevated token; run \\\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   │...\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*49│    /// Not elevated (or no de-elevation target exists): spawn as-is.\n 50│    Inherit,\n*51│    /// Elevated with an unelevated identity available: spawn de-elevated.\n 52│    DeElevate,\n 53│}\n 54│\n*55│/// The pure decision: de-elevate iff the process is elevated AND an\n*56│/// unelevated identity exists to drop to (Windows: the UAC linked token;\n*57│/// Linux: a parsed `SUDO_UID`). Elevated-without-target spawns `Inherit`\n*58│/// — a consistent elevated universe with a loud warning beats a torn one.\n#### pairhost.rs\n 116│            // live ceremony connection. A completed re-pair of a tombstoned key\n*117│            // clears the tombstone (decision 10) — the deliberate elevated act.\n 118│            // [impl->REQ-MESH-2]\n 119│            roster.clear_tombstone(&outcome.subnet, &outcome.joiner_hex);\n 120│            record_roster_peer(\n#### service.rs\n  81│/// `daemon run` is foreground by contract (REQ-DAEMON-7). It refuses ONLY when\n *82│/// it would otherwise vanish: on Windows, when an elevated invocation has a\n *83│/// real de-elevation target (it would respawn detached/de-elevated, KH 5.7).\n *84│/// A uniformly-elevated or headless Windows session has NO target — there `run`\n *85│/// serves elevated in the foreground consistently (and must, e.g. an elevated\n  86│/// self-hosted CI runner with no desktop shell). Unix always serves foreground\n  87│/// (privileges drop in-process). So the refuse is gated on \"would vanish\", not\n *88│/// on elevation alone.\n  89│// [impl->REQ-DAEMON-7]\n  90│pub fn run_refused_when_vanishing(is_windows: bool, would_vanish: bool) -> bool {\n  91│    is_windows && would_vanish\n    │...\n 389│    // [unit->REQ-DAEMON-7] `run` refuses only when it WOULD VANISH: on Windows\n*390│    // with a real de-elevation target. A headless/uniformly-elevated Windows\n 391│    // session (no target → would_vanish=false) serves foreground, as does unix.\n 392│    #[test]\n 393│    fn run_refused_only_when_windows_and_vanishing() {\n\n### tests/\n#### brain_swap.rs\n 18│//! **Filename note:** this file is `brain_swap.rs`, NOT `update.rs`, on purpose.\n*19│//! Windows' installer-detection heuristic (UAC) forces elevation for any\n 20│//! executable whose name contains `update`/`setup`/`install`/`patch`; a test\n 21│//! binary `update-<hash>.exe` then fails to launch with `os error 740` (requires\n*22│//! elevation). Do not rename this to match the module under test.\n 23│//!\n 24│//! **REQ-UPD-3 `int` re-pointed (restoration D7-1).** This test proves the\n 25│//! *in-process* brain-only swap shape (the engine's `apply_brain_only` over an\n\n## spt-msg/src/\n### lib.rs\n 10│//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the\n*11│//! mis-elevated legacy reply-to framing). The delivery routing, listener\n 12│//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against\n 13│//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).\n 14│//!\n### wire.rs\n 3│//! The frame carries `(from, body)` **structurally** — there is no\n*4│//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated\n 5│//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in\n 6│//! [`spt_proto::event`], composed once at the delivery edge, never on the wire).\n 7│//! spt-core mandates **no** byte-for-byte TCP interop with any sister `spt`.\n\n## spt-store/src/\n### roster.rs\n  37│//! Cross-node convergence of an *un*-tombstone is **not** D3's concern.\n *38│//! [`clear_tombstone`] is a *local* elevated act (the re-pair). The compromised\n  39│//! path re-pairs under a **fresh, never-tombstoned key** (the milestone's\n  40│//! decision 10), so the old tombstone correctly stands forever; the benign\n  41│//! N-1 re-seed + timeboxed rotation propagation is **Mesh-D7**. D3 tests\n    │...\n 286│    /// **Clear-on-repair**: drop the tombstone for `(subnet, pubkey)` — the\n*287│    /// deliberate elevated act a completed re-pair performs. Returns whether one\n 288│    /// was cleared. (A local op; see the module's convergence-boundary note.)\n 289│    // [impl->REQ-MESH-2]\n 290│    pub fn clear_tombstone(&mut self, subnet: &str, pubkey_hex: &str) -> bool {\n\n## spt/\n### Cargo.toml\n  32│# update` command. NOTE: the bin name + the test name deliberately avoid the\n *33│# substring \"update\" — Windows' installer-detection heuristic forces UAC elevation\n  34│# on a manifest-less exe whose name contains it (os error 740 in CI).\n  35│[[bin]]\n  36│name = \"post_step_fixture\"\n    │...\n 105│\n*106│# OS privilege-elevation probe for the node-bound code fetch (REQ-PAIR-6). Only the\n 107│# binary pulls the OS deps; the core stays dep-minimal. Matches the workspace's existing\n 108│# libc/windows-sys usage (spt-msg, spt-store) rather than the heavyweight `windows`.\n 109│[target.'cfg(unix)'.dependencies]\n    │...\n 122│    \"Win32_System_Threading\",\n*123│    # REQ-ELEVATE-1: ShellExecuteW(\"runas\") launches the UAC-elevated console;\n*124│    # GetConsoleProcessList lets the elevated child detect a fresh (sole-owner)\n 125│    # console so it pauses before exit instead of vanishing.\n 126│    \"Win32_UI_Shell\",\n 127│    \"Win32_UI_WindowsAndMessaging\",\n\n### src/\n#### cli.rs\n   30│\n  *31│use crate::elevation::{self, Elevation, Gate};\n   32│use crate::roster;\n   33│\n   34│/// Product tag embedded in the `otpauth://` provisioning URI (the authenticator-app\n     │...\n   37│\n  *38│/// `spt subnet show-code`/`create` refused the fetch for lack of OS elevation. Distinct\n   39│/// from the crate-wide codes (`0` ok · `1` runtime refusal · `2` usage): `3` says the\n  *40│/// fetch was *refused, not failed* — re-run elevated, or read the code from your\n   41│/// authenticator app.\n  *42│const EXIT_NOT_ELEVATED: i32 = 3;\n   43│\n   44│#[derive(Parser)]\n   45│#[command(\n     │...\n 1047│    /// code, the `otpauth://` provisioning URI, and a terminal QR of it.\n*1048│    /// Gated behind OS elevation (the seed-reveal path).\n 1049│    Create {\n 1050│        /// The new subnet's name.\n 1051│        name: String,\n     │...\n 1056│    /// `create` — current code, `otpauth://` URI, terminal QR, expiry.\n*1057│    /// Gated behind OS elevation (or read the code from your authenticator\n 1058│    /// app). With no name the node's sole subnet is used; if it holds\n 1059│    /// several, the name is required (never guessed).\n 1060│    ShowCode {\n     │...\n 1068│    /// name and code when omitted (interactive terminals). Gated behind\n*1069│    /// OS elevation — joining enrolls this whole machine.\n 1070│    Join {\n 1071│        /// The subnet to join (as named on the member machine).\n 1072│        name: Option<String>,\n     │...\n 1085│    /// Removes the subnet's seed, its trust rows, its serve-state, and its\n*1086│    /// registry snapshot here. Gated behind OS elevation (membership exit\n 1087│    /// destroys trust material). The remaining members still hold the old\n 1088│    /// seed — rotate it there if this machine should not rejoin.\n 1089│    Leave {\n     │...\n 1097│    /// tick. Takes a full pubkey hex, an unambiguous prefix, or a node\n*1098│    /// label. Gated behind OS elevation (trust mutation).\n 1099│    Prune {\n 1100│        /// The dead identity: pubkey hex, unambiguous prefix, or label.\n 1101│        node: String,\n     │...\n 1111│    /// locked out and must re-pair. Each target is a pubkey hex, an\n*1112│    /// unambiguous prefix, or a label. Gated behind OS elevation.\n 1113│    Revoke {\n 1114│        /// The identities to revoke: pubkey hex, unambiguous prefix, or label.\n 1115│        #[arg(required = true)]\n     │...\n 3870│/// Run the per-machine daemon (`spt daemon run`, M8 decision 5). FOREGROUND +\n*3871│/// unelevated by contract (REQ-DAEMON-7): this process IS the daemon, blocking\n 3872│/// the broker + seed-control channels until signalled. Never auto-detaches —\n*3873│/// the background/de-elevated path lives in `start`.\n 3874│///\n 3875│/// Two guards before binding:\n*3876│/// - Windows + elevated ⇒ REFUSE (exit 2). An elevated `run` would otherwise\n*3877│///   respawn detached/de-elevated (KH 5.7) and vanish into the background.\n 3878│/// - A managed service is already active ⇒ warn loudly (running inline fights\n 3879│///   it for the socket — the kitsubito 2026-06-08 restart-loop).\n 3880│// [impl->REQ-DAEMON-3]\n     │...\n 3901│    // detached-in-fact (no console OR a piped stderr) EVEN IF `--detached` was not\n*3902│    // passed — the stale-bare-at-logon-task / bare-deelevated-respawn / any-unknown\n 3903│    // rung case. Runs AFTER detach_console (idempotent) and BEFORE any bind/output, so\n 3904│    // a filling inherited pipe can never block the daemon. A foreground human keeps\n 3905│    // their console (both checks false).\n     │...\n 3912│        true,\n*3913│        spt_daemon::deelevate::has_deelevation_target(),\n 3914│    ) {\n*3915│        // Only the vanishing case: elevated WITH an unelevated desktop shell to\n*3916│        // drop to (run would respawn detached/de-elevated). A headless or\n*3917│        // uniformly-elevated session has no target and serves foreground below.\n 3918│        eprintln!(\n*3919│            \"DAEMON_RUN_ELEVATED: `daemon run` is foreground; from an elevated shell it would \\\n 3920│             respawn in the background — use `spt daemon start` for a background daemon, or run \\\n#### elevation.rs\n *1│//! OS privilege-elevation gate for the node-bound pairing-code fetch (REQ-PAIR-6).\n  2│//!\n  3│//! Surfacing a subnet's live TOTP code off a trusted node is the *privileged* fetch\n *4│//! path: it hands out subnet-joining authority, so the process must be elevated\n  5│//! (Windows UAC / Linux root-or-equivalent). When it is not, we refuse the node-bound\n  6│//! fetch and point the user at their authenticator app — where the same seed was\n  7│//! provisioned at pairing (the `otpauth://` URI), giving the code without privilege.\n   │...\n 12│//! cannot run hermetically in CI (the test process's real euid/token is whatever the\n*13│//! runner happens to be). So the *decision* — does this elevation level show the code\n 14│//! or fall back? — is factored into [`gate_for`], a pure function unit-tested both\n*15│//! ways. An unknown/unsupported platform maps to [`Elevation::Unknown`], which\n*16│//! [`gate_for`] treats as NOT elevated: **fail safe** — never leak the code when we\n 17│//! cannot positively confirm privilege.\n 18│// [impl->REQ-PAIR-6]\n 19│\n   │...\n 21│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n*22│pub enum Elevation {\n*23│    /// Confirmed elevated: Windows token is elevated, or Unix euid == 0.\n*24│    Elevated,\n*25│    /// Confirmed NOT elevated.\n*26│    NotElevated,\n 27│    /// Could not determine (unsupported platform, or the OS query failed). Treated as\n*28│    /// NOT elevated by [`gate_for`] — we never assume privilege we cannot confirm.\n 29│    ///\n 30│    /// Constructed only on Windows (an `OpenProcessToken`/`GetTokenInformation`\n 31│    /// failure) and on unsupported platforms; on Unix `geteuid` cannot fail, so the\n   │...\n 40│pub enum Gate {\n*41│    /// Elevated — render the code + `otpauth://` URI.\n 42│    Show,\n*43│    /// Not elevated (or undeterminable) — refuse, point at the authenticator app.\n 44│    Fallback,\n 45│}\n 46│\n*47│/// The pure gate decision (the testable seam). Only a *positively confirmed* elevation\n*48│/// shows the code; `NotElevated` and `Unknown` both fall back (fail safe).\n 49│// [impl->REQ-PAIR-6]\n*50│pub fn gate_for(elevation: Elevation) -> Gate {\n*51│    match elevation {\n*52│        Elevation::Elevated => Gate::Show,\n*53│        Elevation::NotElevated | Elevation::Unknown => Gate::Fallback,\n 54│    }\n 55│}\n 56│\n   │...\n 61│    format!(\n*62│        \"ELEVATION_REQUIRED: showing the code for subnet '{subnet}' off this node needs an \\\n#### main.rs\n  11│mod cli;\n *12│mod elevation;\n  13│mod helpfmt;\n  14│mod picker;\n  15│mod rc;\n    │...\n  66│\n *67│    // The sudo de-elevation drop (KNOWN-HAZARDS 5.7) runs BEFORE anything\n  68│    // touches state: a sudo'd `spt` otherwise lives in root's universe\n  69│    // (sudo flips $HOME, so spt_home — state, node identity, the daemon\n  70│    // socket names — all resolve wrong) and roots whatever it writes. Drop\n *71│    // to the ELECTED default user (uid/gid/HOME) while keeping the elevation\n  72│    // PROOF for the gated commands (`subnet create`/`join`, show-code):\n  73│    // the user paid for the gate by sudo'ing; the drop is state safety,\n  74│    // not authz revocation. Genuine root (no SUDO_*) is left alone — root's\n    │...\n  78│    // account ELECTION (M8 decision 8, REQ-INSTALL-6): `daemon_target_user`\n *79│    // — not the bare `sudo_invoker` — so the first elevated `spt` records\n  80│    // `/etc/spt-core/default-user` and every later one drops deterministically\n  81│    // to that account. (The Daemon::run / spawn_detached election guards run\n  82│    // AFTER this drop — they always see euid != 0 and no-op; electing only\n    │...\n  85│    // persisted because this early drop shadowed both daemon-side guards.)\n *86│    // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n  87│    // [impl->REQ-INSTALL-6]\n  88│    #[cfg(unix)]\n  89│    if let Some(invoker) = spt_daemon::deelevate::daemon_target_user() {\n    │...\n  91│            Ok(()) => {\n *92│                // Keep the elevation PROOF for the gated commands, but stay\n  93│                // silent on the routine drop (the one-time election itself\n *94│                // announces via `ELECTED_DEFAULT_USER`): a \"DEELEVATED:\n  95│                // running as uid N\" line only confuses an end user who just\n  96│                // wanted to create/join a subnet.\n *97│                elevation::mark_proven();\n  98│            }\n  99│            // A drop target existed but the drop failed: continuing would\n 100│            // run root over the user's universe — a known-torn state.\n    │...\n 103│            Err(e) => {\n*104│                eprintln!(\"DEELEVATE_FAIL: {e} — refusing to run elevated over a user universe\");\n 105│                std::process::exit(1);\n 106│            }\n 107│        }\n\n### tests/\n#### composite_e2e.rs\n 13│//! NOTE: the file/binary name deliberately avoids the substring \"update\" —\n*14│//! Windows' installer-detection heuristic forces UAC elevation on a\n 15│//! manifest-less exe whose name contains it (os error 740; the\n 16│//! post_step_fixture precedent).\n 17│//\n#### oneliner_e2e.rs\n  9│//! (File is named `oneliner_e2e`, not `install_e2e`: Windows' UAC\n*10│//! installer-detection heuristic demands elevation for any executable whose\n 11│//! name contains \"install\" — the test binary inherits the file name and\n 12│//! os error 740s before it can run.)\n 13│//!\n#### resume_no_control_steal_e2e.rs\n 87│//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows installer-detection\n*88│//! demands UAC elevation for such an exe → `os error 740` at `--list` (KH win-update-exe-\n 89│//! uac740). This file is deliberately `resume_no_control_steal_e2e`.\n 90│\n 91│use std::path::PathBuf;\n#### trial_drain_drive_e2e.rs\n  96│//! NAMING (do NOT rename to include \"update\"/\"setup\"/\"install\"): Windows\n *97│//! installer-detection heuristics demand UAC elevation for a test exe whose name\n *98│//! contains those tokens → `os error 740` \"requires elevation\" at `--list` (KH\n  99│//! win-update-exe-uac740). This file is deliberately `trial_drain_drive_e2e`.\n 100│\n 101│use std::path::PathBuf;\n\n#### fixtures/\n##### post_step_fixture.rs\n 5│//! excludes it). NOT a production binary. The bin name avoids the substring\n*6│//! \"update\" — Windows installer-detection forces UAC elevation on a manifest-less\n 7│//! exe whose name contains it (os error 740).\n 8│//!\n 9│//! Contract (spt-core → here): one stdin JSON line = the published seam\n\n# docs/\n## KNOWN-HAZARDS.md\n 212│\n*213│### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\n*214│<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\n*215│- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill ...\n*216│- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run`...\n*217│- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n*218│- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n 219│\n 220│<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n 221│### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n 222│- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\n*223│- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\n 224│- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\n 225│- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call si...\n 226│- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\n    │...\n 235│### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\n*236│- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot...\n*237│- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interact...\n*238│- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n*239│- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n*240│- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n 241│- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\n 242│\n 243│### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n*244│- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-el...\n*245│- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to th...\n*246│- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one qu...\n*247│- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this compos...\n 248│- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n 249│\n 250│<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\n    │...\n 694│| 5.6 | Detached long-lived children spawn `bInheritHandles=FALSE` | daemon + shell spawn |\n*695│| 5.7 | Daemon always unelevated in the invoker's universe (de-elevated spawn + entry guard) | `deelevate` seam, `spawn_detached`, `Daemon::run` |\n 696│| 5.8 | `CREATE_NO_WINDOW` on every console child of the console-less daemon | `gitrun::run_git`, `kill_shell_pid`, `run_bounded_command`, `shellwake` |\n 697│| 5.12 | Native-PTY spawn PATHEXT-resolves a bare program + wraps non-PE targets (.cmd/.bat→`cmd /c`, .ps1→powershell), bypassing portable-pty's shim-first `which` (CreateProcessW os error 193) | `spt_term::winprog` → `PtySession::spawn_program_in` |\n 698│| 6.1 | Single path/registry source of truth | storage layout |\n    │...\n 713│| 7.9 | A daemon-state wire change (e.g. the v0.9.0 agnostic Seed) needs a deliberate broker restart — the resident old broker can't read new bytes; the seed-skew EOF surfaces an actionable `spt daemon stop` hint, not \"failed to fill whole buffer\"; forward: additive + serde-default daemon-state | `cmd_seed`/`seed_fail_message`, broker seed-control residency |\n*714│| 7.10 | A view is independent from the endpoint: the cold-started daemon is launched JOB-NEUTRAL (WMI `Win32_Process.Create` PRIMARY → WmiPrvSE child, outside any terminal job from birth; breakaway DEMOTED to a fallback rung because a job CAN deny it). A WMI/scheduler child does NOT inherit transient shell env, so `SPT_*` (esp. `SPT_HOME`) is forwarded via a `cmd /c set … & start /b` wrapper. Ladder (first-success-wins, both cold-start + `spt daemon start`): WMI → schtasks → breakaway → in-job...\n 715│| 7.11 | A dead PTY child + a dropped operator pump does NOT wedge the broker — PROVE-DON'T-CHANGE: send_stream already QUIC-deadline-bounded (7.8), the loopback duplex is drained broker-internally (evict-not-park), block_on parks the dispatch thread not a net worker; dead endpoint offlined within a reconcile tick | int `attach_wedge_e2e.rs` |\n 716│| 7.12 | Controller output delivered OFF the drain thread (dedicated writer + bounded deadline-detach), never inline under the log lock — a backed-up controller can't wedge the session | broker `OutputLog::append`/`controller_writer`, int `inject_control_wedge.rs` |\n 717│| 7.13 | `spt rc` maps Windows legacy-console delete keys so Backspace→char-delete / Ctrl+Backspace→word-delete; does NOT enable VT console input (= win32-input-mode on WT, broke detach). SUPERSEDED on Windows by 7.16 (the byte-swap `normalize_key_byte` removed; behavior native in `translate_key_event`) | `rc.rs` → 7.16 `translate_key_event` |","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784197385439}}
{"type":"message","id":"8c3ef5c6","parentId":"5e3debfc","timestamp":"2026-07-16T10:23:17.343Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Tracing elevated endpoint lifecycle**"},{"type":"toolCall","id":"call_8h4xVGKkpZHNyivZzthZEIX3|fc_017bff223290b7ba016a58b1155034819abb160fdcc709ba69","name":"grep","arguments":{"i":"Finding endpoint privilege metadata","pattern":"admin|privilege|token|integrity|elevat","path":"crates/spt-store/src/info.rs; crates/spt-store/src/registry.rs; crates/spt-net/src/net/registry.rs; crates/spt/src/api/startup.rs; crates/spt/src/api/delivery.rs; crates/spt-msg/src","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_KWGZVibikAv0Sk4ecqZl1Qo0|fc_017bff223290b7ba016a58b1155044819a8b8cb9360ebca3bf","name":"grep","arguments":{"i":"Finding endpoint establishment symbols","pattern":"pub fn establish_perch|fn establish_perch|register_address|unregister_address|ready|listener|TcpListener|Relay::bind|deliver_tcp|cmd_listen|cmd_bind","path":"crates/spt/src/api; crates/spt-msg/src; crates/spt-store/src/registry.rs; crates/spt-store/src/info.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_47i9o3X01YeBGiS0WdiZxVpN|fc_017bff223290b7ba016a58b115504c819aa200ef5903417f09","name":"grep","arguments":{"i":"Finding send route symbols","pattern":"fn cmd_send|pub fn send|fn send|resolve_|wansend|spool_message|deliver_tcp|try_spt_hosted_inject|active_only","path":"crates/spt/src/cli.rs; crates/spt/src/wansend.rs; crates/spt-msg/src; crates/spt-daemon/src/msg.rs; crates/spt-daemon/src/inject.rs; crates/spt-net/src/net/wanmsg.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_P1OR6n8pmTUXbOV8tFFU5Wal|fc_017bff223290b7ba016a58b1155054819ab50703ec7d54ded0","name":"grep","arguments":{"i":"Finding endpoint elevation docs","pattern":"elevated endpoint|harness-hosted-elevated|talk-down|listener|adapter attachment|attach-on-session|unbound endpoint","path":"CONTEXT.md; docs/KNOWN-HAZARDS.md; docs/adr","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":25356,"output":547,"cacheRead":57856,"cacheWrite":0,"totalTokens":83759,"reasoningTokens":103,"cost":{"input":0.12678,"output":0.01641,"cacheRead":0.028928,"cacheWrite":0,"total":0.17211800000000002}},"stopReason":"toolUse","timestamp":1784197385485,"responseId":"resp_017bff223290b7ba016a58b109c468819a9f7d48c9ac5a1520","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b10b7294819a920489c97c4d054c","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLEVSd_zQzzfCk9rU1uZY0lcyP9KJLUjizjwMEhzQ804GtkN-6C38cjXX7VXLaHW0p1fLkwgmTDMOVInA02nqR1lP98RSlmBzA0F5rJ7N889tqXLkrZ-NrNjzC15Ngw7u-uBc-P30uTgN85_4E8_h8NLMegXqCtsadu5K2OcOYuci7dDlJXweXlcfPuEpgovf5-YTfNtZ6qIqIiOh5_p1VmJUeXX3624KI34AgS6YfJG2R94sPEUcL5koLeS0PLMDWZ3uCaP2iIGEx9Ew4pzb-oDfZEsG5fx02eViWVK-Zva3dbzbcmSJ-F6ndR0M_T14IonuaEpLTlXy5QUC9SxFAOVGoOUXTEg9LPO0YuHdHUOWOLJ1oNXoGRRlxjbhs0NacFUYj_gIxEADd9zRZ4Ue5zVkOuY3H0ILjrbL7ZdD1sPHuH10ocsHWCLzRaGvUhS0PPACpDQlpR5PPtfV7yH3c92c2knLTkxfEw2L5icG0Q47DK4ei2eoY5cU81QO6L9O6zRZYNrPgu_3EDVrb1hTPBKiu7l368kfkKc_02p1-UoFZAHv8WDbuf8iO5DenqMTDLzKP3TpRUn3w9Thalr5o0bpjR4guLD0kYhXmV6eC-WYYZ0cFoJPIPlTKVLcCflOA1hEfQ0HrF70Td4QJZyWf9FCyMxVSZ8CI1dxKs_RHV3_FM-kwLroiCLI4VGs9sUhtGVao_2hmXz5ztyU7sOv7vJfqB5sDbG5NDQ2Rb-U2KF43MajsCfeLGuXtLu5jZe0gTwMoBVDU6DotvM5a0REeu2-tf6MmILbcPDDwt_2-l5G5_ZCpPLcgE4BMaowum4Y6zixb1MbSAJiKMBfYJIM1thyQCIXU9HxUzCuRg-tvTCwGgZg4h_Xya7h2MgvzN82c2YzKJt89F4n3PhR47xAYsG2GSHS8GtBx_HpDgJmsbKclQ3v3U5q1QeUMrNUoyZuigX6kXELLHTLwAY5HZl6nhMAso4bB6LwlbbRd8gDXVWGTQDRpW6zBFzzXb9LS3GI53p44l4n73CY64AEqMCFWDMQ0C6xndaywhmgo0eKWVupNIdgm_hkFlznURpdb6M5JNbb0TWUMxof2JkopRicJTO96U-H2KgpeGUqCnmxwP6Ki0Yif_sSdmOFzszTiwdAT3sbxZasCCX0yvcCYvZ5SSLus_NylIaWsWofjKK7oGc2Frsse_zXvEzTLEgm6rUE-mlcaL5cU1ZzfNATzzUILCmrY6BQsaCC50o90kKVcT41P7mGzycsUtz3pwc4QVlJAXDj1TyYXZ4vHJh_-YswcNxIQaFlWjox39IE6EjygOnIb02r1SD_cFMFbnvYqkjkXa0N2KSTAnAxnBEQIG7EBGVgGMGPurMqAE_I_0mUbbQmdLHILsddRcydtv4hSNYpsb4lechmXvjR-WHQa-gzG1y26StjUsVob3HrrEuCMQKimyH1DQubQqL5k3VInSf_IdavcNRGxnIe556kWfTP3ZpsdyqX_fVUDkun7ZGWszQEHUrg0MruPn4J2cnSIFhjSHeMfbv5WFvDBPfswtF5wzpenKH6jbSDFQrU5thNIHT7ATosnTBRaD5Q0bU5pH_GiId9Zcy2-fTfHNR_wm6YlSqgg==","summary":[{"type":"summary_text","text":"**Tracing elevated endpoint lifecycle**"}]},{"id":"fc_017bff223290b7ba016a58b1155034819abb160fdcc709ba69","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding endpoint privilege metadata\",\"pattern\":\"admin|privilege|token|integrity|elevat\",\"path\":\"crates/spt-store/src/info.rs; crates/spt-store/src/registry.rs; crates/spt-net/src/net/registry.rs; crates/spt/src/api/startup.rs; crates/spt/src/api/delivery.rs; crates/spt-msg/src\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_8h4xVGKkpZHNyivZzthZEIX3","name":"grep"},{"id":"fc_017bff223290b7ba016a58b1155044819a8b8cb9360ebca3bf","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding endpoint establishment symbols\",\"pattern\":\"pub fn establish_perch|fn establish_perch|register_address|unregister_address|ready|listener|TcpListener|Relay::bind|deliver_tcp|cmd_listen|cmd_bind\",\"path\":\"crates/spt/src/api; crates/spt-msg/src; crates/spt-store/src/registry.rs; crates/spt-store/src/info.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_KWGZVibikAv0Sk4ecqZl1Qo0","name":"grep"},{"id":"fc_017bff223290b7ba016a58b115504c819aa200ef5903417f09","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding send route symbols\",\"pattern\":\"fn cmd_send|pub fn send|fn send|resolve_|wansend|spool_message|deliver_tcp|try_spt_hosted_inject|active_only\",\"path\":\"crates/spt/src/cli.rs; crates/spt/src/wansend.rs; crates/spt-msg/src; crates/spt-daemon/src/msg.rs; crates/spt-daemon/src/inject.rs; crates/spt-net/src/net/wanmsg.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_47i9o3X01YeBGiS0WdiZxVpN","name":"grep"},{"id":"fc_017bff223290b7ba016a58b1155054819ab50703ec7d54ded0","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding endpoint elevation docs\",\"pattern\":\"elevated endpoint|harness-hosted-elevated|talk-down|listener|adapter attachment|attach-on-session|unbound endpoint\",\"path\":\"CONTEXT.md; docs/KNOWN-HAZARDS.md; docs/adr\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_P1OR6n8pmTUXbOV8tFFU5Wal","name":"grep"}]},"duration":11856.495000000112,"ttft":1935.6649000002071,"contextSnapshot":{"promptTokens":83212,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_8h4xVGKkpZHNyivZzthZEIX3|fc_017bff223290b7ba016a58b1155034819abb160fdcc709ba69","toolName":"grep","startedAt":"2026-07-16T10:23:17.343Z","args":{"path":"crates/spt-store/src/info.rs; crates/spt-store/src/registry.rs; crates/spt-net/src/net/registry.rs; crates/spt/src/api/startup.rs; crates/spt/src/api/delivery.rs; crates/spt-msg/src"},"intent":"Finding endpoint privilege metadata"},"id":"ff011bd3","parentId":"8c3ef5c6","timestamp":"2026-07-16T10:23:17.343Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KWGZVibikAv0Sk4ecqZl1Qo0|fc_017bff223290b7ba016a58b1155044819a8b8cb9360ebca3bf","toolName":"grep","startedAt":"2026-07-16T10:23:17.343Z","args":{"path":"crates/spt/src/api; crates/spt-msg/src; crates/spt-store/src/registry.rs; crates/spt-store/src/info.rs"},"intent":"Finding endpoint establishment symbols"},"id":"484fcafc","parentId":"ff011bd3","timestamp":"2026-07-16T10:23:17.343Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_47i9o3X01YeBGiS0WdiZxVpN|fc_017bff223290b7ba016a58b115504c819aa200ef5903417f09","toolName":"grep","startedAt":"2026-07-16T10:23:17.343Z","args":{"path":"crates/spt/src/cli.rs; crates/spt/src/wansend.rs; crates/spt-msg/src; crates/spt-daemon/src/msg.rs; crates/spt-daemon/src/inject.rs; crates/spt-net/src/net/wanmsg.rs"},"intent":"Finding send route symbols"},"id":"ab1d26c4","parentId":"484fcafc","timestamp":"2026-07-16T10:23:17.343Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_P1OR6n8pmTUXbOV8tFFU5Wal|fc_017bff223290b7ba016a58b1155054819ab50703ec7d54ded0","toolName":"grep","startedAt":"2026-07-16T10:23:17.343Z","args":{"path":"CONTEXT.md; docs/KNOWN-HAZARDS.md; docs/adr"},"intent":"Finding endpoint elevation docs"},"id":"049f35e8","parentId":"ab1d26c4","timestamp":"2026-07-16T10:23:17.343Z"}
{"type":"message","id":"dd1b94c0","parentId":"049f35e8","timestamp":"2026-07-16T10:23:17.368Z","message":{"role":"toolResult","toolCallId":"call_47i9o3X01YeBGiS0WdiZxVpN|fc_017bff223290b7ba016a58b115504c819aa200ef5903417f09","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/src/\n### cli.rs\n 81|        /// until then and never surface to the target's active poll.\n*82|        #[arg(long, conflicts_with = \"active_only\")]\n 83|        idle_only: bool,\n 84|        /// Deliver only through the target's own poll (the no-interrupt hook\n 85|        /// channel); never wakes an idle target. Replaces the old --deferred.\n...\n 87|        // as a documented flag in published reference.md, so `spt send --deferred`\n*88|        // keeps parsing (→ active_only) for external scripts/adapters. Hidden (not\n 89|        // `visible_alias`) → it does NOT render in the generated reference.md, so\n 90|        // there is zero docs-drift. (ADR-0028 W2 pure-rename + back-compat.)\n 91|        #[arg(long, alias = \"deferred\")]\n*92|        active_only: bool,\n 93|        /// Drop the message if it cannot be delivered in its window, instead of\n 94|        /// spooling until delivered.\n 95|        #[arg(long)]\n...\n 1293|            idle_only,\n*1294|            active_only,\n 1295|            ephemeral,\n 1296|            prefer_native,\n 1297|            force_native,\n...\n 1303|            idle_only,\n*1304|            active_only,\n 1305|            ephemeral,\n 1306|            prefer_native,\n 1307|            force_native,\n...\n 1404|                // [impl->REQ-RUN-ID-REUSES-ADAPTER]\n*1405|                let target = resolve_run_target(adapter, id, |id| {\n*1406|                    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 1407|                    spt_store::info::read_info(&perch).and_then(|i| i.adapter)\n 1408|                });\n 1409|                match target {\n...\n 1491|fn cmd_spool_audit(id: &str, json: bool) -> i32 {\n*1492|    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 1493|    let rows = match spt_store::spool::audit_rows_at(&perch_path) {\n 1494|        Ok(r) => r,\n 1495|        Err(e) => {\n...\n 1754|// [impl->REQ-RESUME-HARNESS-SESSION-ID]\n*1755|fn resolve_resume_session(requested: &str, ledger_newest_first: &[String]) -> ResumeResolution {\n 1756|    // A non-provisional request is already a real/harness session (the picker's\n 1757|    // chosen ledger row, or an operator-supplied UUID) — resume it directly.\n 1758|    if !spt_daemon::harnesshost::is_provisional_session_id(requested) {\n...\n 1816|// [impl->REQ-RUN-ID-REUSES-ADAPTER]\n*1817|fn resolve_run_target(\n 1818|    adapter: Option<String>,\n 1819|    id: Option<String>,\n 1820|    recorded: impl FnOnce(&str) -> Option<String>,\n...\n 1833|mod run_target_tests {\n*1834|    use super::{resolve_run_target, RunTarget};\n 1835|\n 1836|    fn s(x: &str) -> Option<String> {\n 1837|        Some(x.to_string())\n...\n 1847|    #[test]\n*1848|    fn resolve_run_target_quadrants() {\n 1849|        // (Some,Some) → Direct verbatim, perch never consulted.\n 1850|        assert_eq!(\n*1851|            resolve_run_target(s(\"claude-spt\"), s(\"foo\"), |_| panic!(\"must not read perch\")),\n 1852|            RunTarget::Direct { adapter: \"claude-spt\".into(), id: \"foo\".into() }\n 1853|        );\n 1854|        // (None,Some) + existing perch's recorded adapter → Direct (the D-1 reuse).\n 1855|        assert_eq!(\n*1856|            resolve_run_target(None, s(\"foo\"), |id| {\n 1857|                assert_eq!(id, \"foo\");\n 1858|                s(\"claude-spt\")\n 1859|            }),\n...\n 1863|        assert_eq!(\n*1864|            resolve_run_target(None, s(\"foo\"), |_| None),\n 1865|            RunTarget::Picker { adapter: None, id: s(\"foo\") }\n 1866|        );\n 1867|        // (Some,None) → picker with the adapter pre-selected.\n 1868|        assert_eq!(\n*1869|            resolve_run_target(s(\"claude-spt\"), None, |_| None),\n 1870|            RunTarget::Picker { adapter: s(\"claude-spt\"), id: None }\n 1871|        );\n 1872|        // (None,None) → bare picker.\n 1873|        assert_eq!(\n*1874|            resolve_run_target(None, None, |_| None),\n 1875|            RunTarget::Picker { adapter: None, id: None }\n 1876|        );\n 1877|    }\n...\n 1914|    let adapters_dir = spt_store::perch::adapters_dir();\n*1915|    let (manifest, install_dir) = match spt_runtime::registry::resolve_option(&adapters_dir, adapter)\n 1916|    {\n 1917|        // The record's `source_dir` is the adapter install dir (W3a) — carried to\n 1918|        // the broker so a live adapter update can target this endpoint.\n...\n 1942|    // provisional, so `claude -r <provisional>` hangs at \"no sessions match\" (the\n*1943|    // triage's 02:05 daemon-bounce `claude -r 70b5bfa40901b7d4`). resolve_resume_\n 1944|    // session anchors on the ledger (hook-authored = harness-reported); a request\n 1945|    // that is itself a provisional recovers the latest harness-reported row, and a\n 1946|    // total absence starts FRESH + loud rather than leaking the provisional.\n...\n 1949|        Some(requested) => {\n*1950|            let perch = spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);\n 1951|            // sessions::last_k returns oldest→newest; the resolver wants newest-first.\n 1952|            let mut ledger: Vec<String> =\n 1953|                spt_store::sessions::last_k(&perch, spt_store::sessions::MAX_LEDGER)\n### wansend.rs\n 7|//! contract, KNOWN-HAZARDS 4.3), resolves the target under the D3c policy\n*8|//! ([`resolve_across_visible`]: refuse-and-qualify on ambiguity, never a\n 9|//! guess), and ships one [`WanMessage`] over the broker's QUIC endpoint to\n 10|//! the resolved node. The receiving daemon's dispatcher funnels it through\n 11|//! the access gate + op-id dedup into the target's local delivery\n...\n 27|use spt_net::net::registry::{\n*28|    key_prefix, resolve_across_visible, Ambiguity, Resolution, SubnetRegistry,\n 29|};\n 30|use spt_net::net::wanmsg::WanMessage;\n 31|use spt_proto::addr::Address;\n...\n 109|/// REQ-RC-CROSS-NODE-ATTACH). Mirrors the [`wan_send`]/[`wan_rest`] resolve-dial\n*110|/// discipline exactly (parse → [`resolve_across_visible`] → own-node degrade →\n 111|/// [`seed_first_addr`] → [`Brain::net_dial`]) so the attach leg shares the same\n 112|/// routing — and the same stale-tolerant, refuse-and-qualify semantics — the\n 113|/// message leg already proves. The `net_dial` rides the broker call deadline\n...\n 115|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*116|pub fn resolve_and_dial_owner(brain: &mut Brain, endpoint: &str) -> OwnerDial {\n 117|    let own_hex = match spt_store::nodeid::load_or_create() {\n 118|        Ok(id) => id.public_key().to_hex(),\n 119|        Err(e) => {\n...\n 145|\n*146|    let instance = match resolve_across_visible(&regs, &address, &own_hex, excluded) {\n 147|        Resolution::Resolved { instance, .. } => instance,\n 148|        Resolution::Ambiguous(a) => return OwnerDial::Ambiguous(render_refusal(&address.id, &a, &regs)),\n 149|        Resolution::NotFound => return OwnerDial::NotFound,\n...\n 236|\n*237|    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {\n 238|        Resolution::Resolved { instance, .. } => instance,\n 239|        // Refusals render as valid copy-paste targets (REQ-INST-10), never a\n 240|        // debug dump the user can't act on.\n...\n 408|\n*409|    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {\n 410|        Resolution::Resolved { instance, .. } => instance,\n 411|        Resolution::Ambiguous(a) => {\n 412|            return WanRestOutcome::Ambiguous(render_refusal(&address.id, &a, regs))\n...\n 541|\n*542|    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {\n 543|        Resolution::Resolved { instance, .. } => instance,\n 544|        Resolution::Ambiguous(a) => {\n 545|            return WanShellLinkOutcome::Ambiguous(render_refusal(&address.id, &a, regs))\n...\n 929|        let (a_name, b_name) = (\n*930|            format!(\"spt-wansend-a-{}.sock\", std::process::id()),\n*931|            format!(\"spt-wansend-b-{}.sock\", std::process::id()),\n 932|        );\n 933|        let _a = net_broker(&a_name, dir.path());\n 934|        let _b = net_broker(&b_name, dir.path());\n...\n 946|            let registry = Arc::new(RegistryHost::new_at(\n*947|                \"b-wansend\",\n 948|                EpochSource::load_from(&dir.path().join(\"b-epoch.json\")),\n 949|                dir.path().join(\"b-registry\"),\n 950|            ));\n...\n 957|        let target = \"ling-wan\";\n*958|        let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n 959|        std::fs::create_dir_all(&perch_path).unwrap();\n 960|        let rec = InfoJson::new(target, \"now\", std::process::id(), \"s\", \"ready_agent\");\n 961|        info::write_info(&perch_path, &rec).unwrap();\n...\n 1084|        let target = \"ling-rest\";\n*1085|        let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n 1086|        std::fs::create_dir_all(&perch_path).unwrap();\n 1087|        let rec = InfoJson::new(target, \"now\", std::process::id(), \"s\", \"live_agent\");\n 1088|        info::write_info(&perch_path, &rec).unwrap();\n*1089|        spool::spool_message_deferred_at(&perch_path, \"psyche\", \"deferred brief\").unwrap();\n*1090|        spool::spool_message_at(&perch_path, \"bob\", \"live one\").unwrap();\n 1091|\n 1092|        let mut a = connect_retry(&a_name);\n 1093|        let mut b_probe = connect_retry(&b_name);\n...\n 1334|    /// Write a registry snapshot to the canonical on-disk path\n*1335|    /// [`resolve_and_dial_owner`] reads via [`load_snapshots`] — the production\n 1336|    /// disk path, not an injected map.\n 1337|    fn write_snapshot(subnet: &str, id: &str, node: &str) {\n 1338|        let dir = perch::identity_dir().join(\"registry\");\n...\n 1354|    #[test]\n*1355|    fn resolve_and_dial_owner_is_honest_on_resolution_misses() {\n 1356|        let _home = isolated_home();\n 1357|        let dir = tempfile::tempdir().unwrap();\n 1358|        let name = format!(\"spt-rcresolve-{}.sock\", std::process::id());\n...\n 1363|        assert!(matches!(\n*1364|            resolve_and_dial_owner(&mut a, \"ghost\"),\n 1365|            OwnerDial::NotFound\n 1366|        ));\n 1367|\n...\n 1374|        assert!(matches!(\n*1375|            resolve_and_dial_owner(&mut a, \"mine\"),\n 1376|            OwnerDial::NotFound\n 1377|        ));\n 1378|\n...\n 1382|        assert!(matches!(\n*1383|            resolve_and_dial_owner(&mut a, \"twin\"),\n 1384|            OwnerDial::Ambiguous(_)\n 1385|        ));\n 1386|    }\n\n## spt-msg/src/\n### deliver.rs\n 9|//! Routing path (the REQ-MSG-1 contract):\n*10|//! 1. `resolve_address` (stale-clean first) maps the target id -> a registered\n 11|//!    `127.0.0.1:<port>`.\n 12|//! 2. TCP `connect_timeout` + framed write. **Success is the only thing that\n 13|//!    counts as a live delivery.**\n...\n 68|pub fn perch_exists(target: &str) -> bool {\n*69|    perch::resolve_info_file(target, ParentHint::Infer).exists()\n 70|}\n 71|\n 72|/// True if `target` is online: its `ready` marker exists and the single liveness\n...\n 80|pub fn is_online(target: &str) -> bool {\n*81|    if !perch::resolve_ready_file(target, ParentHint::Infer).exists() {\n 82|        return false;\n 83|    }\n*84|    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n 85|    spt_store::liveness::is_perch_alive(&perch_path)\n 86|}\n 87|\n...\n 95|// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]\n*96|pub fn deliver_tcp(target: &str, from: &str, body: &str, owlery: &Path) -> bool {\n*97|    if let Some(addr_str) = registry::resolve_address(target, owlery) {\n 98|        if let Ok(addr) = addr_str.parse::<SocketAddr>() {\n 99|            match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {\n 100|                Ok(mut stream) => {\n...\n 131|    //    double-delivery).\n*132|    if deliver_tcp(target, from, body, owlery) {\n 133|        return DeliveryPath::Tcp;\n 134|    }\n 135|\n 136|    // 2. Spool fallback — the poll drain delivers it when the target starts.\n*137|    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n*138|    let _ = spool::spool_message_at(&perch_path, from, body);\n 139|    DeliveryPath::Spool\n 140|}\n 141|\n...\n 149|// [impl->REQ-MSG-1]\n*150|pub fn send(target: &str, from: &str, body: &str, owlery: &Path) -> SendOutcome {\n 151|    let trimmed = body.trim();\n 152|    if trimmed.is_empty() {\n 153|        return SendOutcome::Empty;\n...\n 174|// [impl->REQ-MSG-1]\n*175|pub fn send_deferred(target: &str, from: &str, body: &str) -> SendOutcome {\n 176|    let trimmed = body.trim();\n 177|    if trimmed.is_empty() {\n 178|        return SendOutcome::Empty;\n...\n 182|    }\n*183|    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n*184|    let _ = spool::spool_message_deferred_at(&perch_path, from, trimmed);\n 185|    SendOutcome::Queued\n 186|}\n 187|\n 188|/// Window-aware send (ADR-0028 W2). `window` is the delivery-window axis\n*189|/// (`default` | `idle_only` | `active_only`); `ephemeral` is the persistence axis\n 190|/// (carried on the row; evaporation wires in W3 with the channel axis).\n*191|/// - `active_only` is the hook channel — spool-only, NO live TCP/wake (= the\n 192|///   former `--deferred`).\n 193|/// - `default` / `idle_only` try a live TCP handoff first (the relay IS the idle\n 194|///   wake), then fall back to the spool TAGGED with the window so the drain\n...\n 199|// [impl->REQ-MSG-DELIVERY-AXES]\n*200|pub fn send_windowed(\n 201|    target: &str,\n 202|    from: &str,\n 203|    body: &str,\n...\n 214|    }\n*215|    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n*216|    // active_only never wakes — spool straight to the hook channel.\n*217|    if window != spool::WINDOW_ACTIVE_ONLY && deliver_tcp(target, from, trimmed, owlery) {\n 218|        return SendOutcome::Sent;\n 219|    }\n*220|    let _ = spool::spool_message_windowed_at(\n 221|        &perch_path,\n 222|        from,\n 223|        trimmed,\n### listener.rs\n 12|//! address survives a crashed listener — the recycled-pid guard\n*13|//! ([`spt_store::registry::resolve_address`]) is the second line of defense.\n 14|//!\n 15|//! **Interim model (M1):** one standalone listener per ready agent. M3's\n 16|//! `spt-daemon` consolidates all listeners into one supervisor; the surface is\n...\n 94|        let owlery = perch::owlery_dir();\n*95|        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 96|\n 97|        let listener = TcpListener::bind(\"127.0.0.1:0\")\n 98|            .map_err(|e| format!(\"Failed to bind TCP listener: {}\", e))?;\n### ready.rs\n 66|        spt_proto::id::validate_endpoint_id(id).map_err(|e| e.to_string())?;\n*67|        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 68|        fs::create_dir_all(&perch_path)\n 69|            .map_err(|e| format!(\"Failed to create perch dir: {}\", e))?;\n 70|\n...\n 105|        // ready marker — the liveness signal senders check.\n*106|        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\n 107|            .map_err(|e| format!(\"Failed to write ready marker: {}\", e))?;\n 108|\n 109|        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind\n...\n 202|    pub fn soft_cleanup(&mut self) {\n*203|        let _ = fs::remove_file(perch::resolve_ready_file(&self.id, ParentHint::Infer));\n 204|        self.listener.close();\n 205|        self.closed = true;\n 206|    }\n...\n 266|        // info.json + ready marker on disk.\n*267|        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 268|        assert!(info::read_info(&perch_path).is_some());\n*269|        assert!(perch::resolve_ready_file(&id, ParentHint::Infer).exists());\n 270|        drop(agent);\n 271|    }\n 272|\n...\n 279|        // Pre-create an offline perch and spool a message to it.\n*280|        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 281|        fs::create_dir_all(&perch_path).unwrap();\n 282|        let rec = InfoJson::new(&id, \"now\", std::process::id(), \"sess\", \"ready_agent\");\n 283|        info::write_info(&perch_path, &rec).unwrap();\n...\n 329|        let (mut agent, _) = ReadyAgent::start(&id).unwrap();\n*330|        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 331|\n 332|        // A deferred row survives the startup drain — use it to prove the spool\n 333|        // is preserved across cleanup.\n*334|        spool::spool_message_deferred_at(&perch_path, \"hooker\", \"deferred body\").unwrap();\n 335|        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);\n 336|\n 337|        agent.soft_cleanup();\n...\n 339|        // ready marker gone...\n*340|        assert!(!perch::resolve_ready_file(&id, ParentHint::Infer).exists());\n 341|        // ...but spool row + info.json preserved.\n 342|        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);\n 343|        assert!(info::read_info(&perch_path).is_some());\n...\n 355|        let (mut agent, _) = ReadyAgent::start(&id).unwrap();\n*356|        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 357|\n 358|        // The bind wrote exactly one Boot row keyed on the agent's session id.\n 359|        let rows = spt_store::sessions::read_all(&perch_path);\n### ring.rs\n 53|fn from_has_live_perch(from: &str) -> bool {\n*54|    if !perch::resolve_ready_file(from, ParentHint::Infer).exists() {\n 55|        return false;\n 56|    }\n*57|    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 58|    spt_store::liveness::is_perch_alive(&perch_path)\n 59|}\n 60|\n...\n 98|    // Block for the reply: poll the listener and the spool until the deadline.\n*99|    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 100|    let deadline = Instant::now() + timeout;\n 101|    let outcome = loop {\n 102|        if let Some((from, body)) = reply_listener.check_message_blocking() {\n...\n 125|fn setup_ephemeral(from: &str) -> Result<PollListener, String> {\n*126|    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 127|    fs::create_dir_all(&perch_path).map_err(|e| format!(\"create ephemeral perch: {}\", e))?;\n 128|    let rec = InfoJson::new(from, \"once\", std::process::id(), \"ring\", \"ready_agent\");\n 129|    info::write_info(&perch_path, &rec).map_err(|e| format!(\"write info.json: {}\", e))?;\n*130|    fs::write(perch::resolve_ready_file(from, ParentHint::Infer), \"\")\n 131|        .map_err(|e| format!(\"write ready marker: {}\", e))?;\n 132|    PollListener::bind_blocking(from, REPLY_POLL)\n 133|}\n...\n 137|fn cleanup_ephemeral(from: &str) {\n*138|    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n*139|    let _ = fs::remove_file(perch::resolve_ready_file(from, ParentHint::Infer));\n*140|    let _ = fs::remove_file(perch::resolve_info_file(from, ParentHint::Infer));\n*141|    let _ = fs::remove_file(perch::resolve_spool_db(from, ParentHint::Infer));\n 142|    // Recursive: the wholly-owned ephemeral perch dir also carries the `.info.lock`\n 143|    // sentinel that write_info now creates (REQ-HAZARD-INFO-RMW-LOST-UPDATE); a\n 144|    // non-recursive remove_dir would leak the dir on that stray file (as every other\n...\n 162|    fn perch_gone(id: &str) -> bool {\n*163|        !perch::resolve_perch_path(id, ParentHint::Infer).exists()\n 164|    }\n 165|\n 166|    // [unit->REQ-MSG-1] ring delivers and returns the reply; ephemeral perch is\n...\n 233|        // Offline perch (exists, no listener).\n*234|        let perch_path = perch::resolve_perch_path(&target, ParentHint::Infer);\n 235|        fs::create_dir_all(&perch_path).unwrap();\n 236|        let rec = InfoJson::new(&target, \"now\", std::process::id(), \"sess\", \"ready_agent\");\n 237|        info::write_info(&perch_path, &rec).unwrap();\n\n## spt-daemon/src/\n### inject.rs\n 5|//! listen` relay (no advertised TCP address). These are exactly the endpoints a\n*6|//! plain delivery would otherwise SILENTLY SPOOL — `deliver_tcp` finds no address\n 7|//! — so they must instead be reached by injecting the `<EVENT>` into the\n 8|//! broker-held PTY through the live translation binary.\n 9|//!\n...\n 12|//! (the cross-node WAN ingress) had NO such leg — it tried the relay\n*13|//! (`deliver_tcp`) then fell straight to spool, so a WAN arrival to an idle\n 14|//! spt-hosted perch with a healthy binary starved until an adapter hook happened\n 15|//! to poll (the \"sent but never lands\" report). This module is the ONE shared\n 16|//! implementation both paths call, so cross-node and local delivery cannot drift.\n...\n 31|/// broker-PTY-`controllable`. A harness-hosted endpoint with a relay\n*32|/// (`resolve_address` is `Some`) takes the normal TCP path; an offline or\n 33|/// non-controllable perch is not a PTY-inject target. Factored verbatim from\n 34|/// `cmd_send`'s predicate so the WAN ingress shares ONE definition.\n 35|// [impl->REQ-WAN-SPT-HOSTED-DELIVERY]\n...\n 40|    }\n*41|    if spt_store::registry::resolve_address(target, owlery).is_some() {\n 42|        return false; // a relay listener exists → normal TCP delivery\n 43|    }\n*44|    spt_store::info::read_info(&spt_store::perch::resolve_perch_path(\n 45|        target,\n 46|        spt_store::perch::ParentHint::Infer,\n 47|    ))\n...\n 74|// [impl->REQ-SEND-SPT-HOSTED]\n*75|pub fn try_spt_hosted_inject(\n 76|    target: &str,\n 77|    from: &str,\n 78|    body: &str,\n...\n 97|/// an operator poke. Identical mechanic to the F-023 idle-EDGE drain: CLAIM the pending\n*98|/// rows (audited, idle-inject leg), offer each through [`try_spt_hosted_inject`]\n 99|/// (`native=false` — the genuine idle window), and on the first miss (no live\n 100|/// translation binary, or the broker still views the endpoint ACTIVE) RELEASE the row\n 101|/// plus all not-yet-attempted back to the spool intact (v0.14.3 LAW: spooled loud, never\n...\n 112|    let perch_path =\n*113|        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);\n*114|    // Resting gate (REQ-INST-6 / KH 1.4): hold the deferred (`active_only`) rows while\n 115|    // the owner rests — the non-deferred set always drains on the idle window.\n 116|    let include_deferred = !crate::resting::deferred_held(&perch_path);\n 117|    // [impl->REQ-SPOOL-TAKE-AUDIT] idle-inject leg — stamp the claim's provenance.\n...\n 131|    for (msg_id, from, body) in rest.by_ref() {\n*132|        match try_spt_hosted_inject(id, &from, &body, owlery, false) {\n 133|            Some((true, _)) => delivered += 1,\n 134|            _ => {\n 135|                release_ids.push(msg_id);\n...\n 166|    let perch_path =\n*167|        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);\n*168|    if !spt_store::perch::resolve_idle_file(id, spt_store::perch::ParentHint::Infer).exists() {\n 169|        return false;\n 170|    }\n 171|    spt_store::spool::pending_count_at(&perch_path).unwrap_or(0) > 0\n...\n 183|    fn establish_hosted(id: &str) -> std::path::PathBuf {\n*184|        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 185|        std::fs::create_dir_all(&perch_path).unwrap();\n 186|        let mut rec = InfoJson::new(id, \"0\", std::process::id(), \"sid\", \"live_agent\");\n 187|        rec.controllable = Some(true);\n 188|        info::write_info(&perch_path, &rec).unwrap();\n*189|        std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 190|        info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 191|        perch_path\n 192|    }\n...\n 270|            // Pending row but NOT idle → skip (the broker's active-gate owns *when*).\n*271|            spt_store::spool::spool_message_at(&perch_path, \"bob\", \"hi\").unwrap();\n 272|            assert!(\n 273|                !has_parked_idle_spool(id, &owlery),\n 274|                \"pending but not idle → the belt skips it\"\n...\n 276|            // Set the `.idle` sentinel → idle + hosted + pending = re-offer this tick.\n*277|            std::fs::write(perch::resolve_idle_file(id, ParentHint::Infer), \"\").unwrap();\n 278|            assert!(\n 279|                has_parked_idle_spool(id, &owlery),\n 280|                \"idle + hosted + pending → the belt re-offers\"\n\n## spt-net/src/net/\n### wanmsg.rs\n 112|/// resolution policy (local-first, refuse-ambiguous, `@node` pin, visibility)\n*113|/// already ran inside `resolve_across_visible`; this only splits its hit into\n 114|/// the local and wire legs.\n 115|// [impl->REQ-NET-1]\n 116|pub fn route_wan(resolution: Resolution, local_node: &str) -> WanRoute {"}],"details":{"scopePath":"crates/spt/src/cli.rs, crates/spt/src/wansend.rs, crates/spt-msg/src, crates/spt-daemon/src/msg.rs, crates/spt-daemon/src/inject.rs, crates/spt-net/src/net/wanmsg.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":100,"fileCount":8,"files":["crates/spt/src/cli.rs","crates/spt/src/wansend.rs","crates/spt-msg/src/deliver.rs","crates/spt-msg/src/listener.rs","crates/spt-msg/src/ready.rs","crates/spt-msg/src/ring.rs","crates/spt-daemon/src/inject.rs","crates/spt-net/src/net/wanmsg.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":20},{"path":"crates/spt/src/wansend.rs","count":20},{"path":"crates/spt-msg/src/deliver.rs","count":20},{"path":"crates/spt-msg/src/listener.rs","count":2},{"path":"crates/spt-msg/src/ready.rs","count":10},{"path":"crates/spt-msg/src/ring.rs","count":11},{"path":"crates/spt-daemon/src/inject.rs","count":16},{"path":"crates/spt-net/src/net/wanmsg.rs","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt/src/\n### cli.rs\n   81│        /// until then and never surface to the target's active poll.\n  *82│        #[arg(long, conflicts_with = \"active_only\")]\n   83│        idle_only: bool,\n   84│        /// Deliver only through the target's own poll (the no-interrupt hook\n   85│        /// channel); never wakes an idle target. Replaces the old --deferred.\n     │...\n   87│        // as a documented flag in published reference.md, so `spt send --deferred`\n  *88│        // keeps parsing (→ active_only) for external scripts/adapters. Hidden (not\n   89│        // `visible_alias`) → it does NOT render in the generated reference.md, so\n   90│        // there is zero docs-drift. (ADR-0028 W2 pure-rename + back-compat.)\n   91│        #[arg(long, alias = \"deferred\")]\n  *92│        active_only: bool,\n   93│        /// Drop the message if it cannot be delivered in its window, instead of\n   94│        /// spooling until delivered.\n   95│        #[arg(long)]\n     │...\n 1293│            idle_only,\n*1294│            active_only,\n 1295│            ephemeral,\n 1296│            prefer_native,\n 1297│            force_native,\n     │...\n 1303│            idle_only,\n*1304│            active_only,\n 1305│            ephemeral,\n 1306│            prefer_native,\n 1307│            force_native,\n     │...\n 1404│                // [impl->REQ-RUN-ID-REUSES-ADAPTER]\n*1405│                let target = resolve_run_target(adapter, id, |id| {\n*1406│                    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 1407│                    spt_store::info::read_info(&perch).and_then(|i| i.adapter)\n 1408│                });\n 1409│                match target {\n     │...\n 1491│fn cmd_spool_audit(id: &str, json: bool) -> i32 {\n*1492│    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 1493│    let rows = match spt_store::spool::audit_rows_at(&perch_path) {\n 1494│        Ok(r) => r,\n 1495│        Err(e) => {\n     │...\n 1754│// [impl->REQ-RESUME-HARNESS-SESSION-ID]\n*1755│fn resolve_resume_session(requested: &str, ledger_newest_first: &[String]) -> ResumeResolution {\n 1756│    // A non-provisional request is already a real/harness session (the picker's\n 1757│    // chosen ledger row, or an operator-supplied UUID) — resume it directly.\n 1758│    if !spt_daemon::harnesshost::is_provisional_session_id(requested) {\n     │...\n 1816│// [impl->REQ-RUN-ID-REUSES-ADAPTER]\n*1817│fn resolve_run_target(\n 1818│    adapter: Option<String>,\n 1819│    id: Option<String>,\n 1820│    recorded: impl FnOnce(&str) -> Option<String>,\n     │...\n 1833│mod run_target_tests {\n*1834│    use super::{resolve_run_target, RunTarget};\n 1835│\n 1836│    fn s(x: &str) -> Option<String> {\n 1837│        Some(x.to_string())\n     │...\n 1847│    #[test]\n*1848│    fn resolve_run_target_quadrants() {\n 1849│        // (Some,Some) → Direct verbatim, perch never consulted.\n 1850│        assert_eq!(\n*1851│            resolve_run_target(s(\"claude-spt\"), s(\"foo\"), |_| panic!(\"must not read perch\")),\n 1852│            RunTarget::Direct { adapter: \"claude-spt\".into(), id: \"foo\".into() }\n 1853│        );\n 1854│        // (None,Some) + existing perch's recorded adapter → Direct (the D-1 reuse).\n 1855│        assert_eq!(\n*1856│            resolve_run_target(None, s(\"foo\"), |id| {\n 1857│                assert_eq!(id, \"foo\");\n 1858│                s(\"claude-spt\")\n 1859│            }),\n     │...\n 1863│        assert_eq!(\n*1864│            resolve_run_target(None, s(\"foo\"), |_| None),\n 1865│            RunTarget::Picker { adapter: None, id: s(\"foo\") }\n 1866│        );\n 1867│        // (Some,None) → picker with the adapter pre-selected.\n 1868│        assert_eq!(\n*1869│            resolve_run_target(s(\"claude-spt\"), None, |_| None),\n 1870│            RunTarget::Picker { adapter: s(\"claude-spt\"), id: None }\n 1871│        );\n 1872│        // (None,None) → bare picker.\n 1873│        assert_eq!(\n*1874│            resolve_run_target(None, None, |_| None),\n 1875│            RunTarget::Picker { adapter: None, id: None }\n 1876│        );\n 1877│    }\n     │...\n 1914│    let adapters_dir = spt_store::perch::adapters_dir();\n*1915│    let (manifest, install_dir) = match spt_runtime::registry::resolve_option(&adapters_dir, adapter)\n 1916│    {\n 1917│        // The record's `source_dir` is the adapter install dir (W3a) — carried to\n 1918│        // the broker so a live adapter update can target this endpoint.\n     │...\n 1942│    // provisional, so `claude -r <provisional>` hangs at \"no sessions match\" (the\n*1943│    // triage's 02:05 daemon-bounce `claude -r 70b5bfa40901b7d4`). resolve_resume_\n 1944│    // session anchors on the ledger (hook-authored = harness-reported); a request\n 1945│    // that is itself a provisional recovers the latest harness-reported row, and a\n 1946│    // total absence starts FRESH + loud rather than leaking the provisional.\n     │...\n 1949│        Some(requested) => {\n*1950│            let perch = spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);\n 1951│            // sessions::last_k returns oldest→newest; the resolver wants newest-first.\n 1952│            let mut ledger: Vec<String> =\n 1953│                spt_store::sessions::last_k(&perch, spt_store::sessions::MAX_LEDGER)\n### wansend.rs\n    7│//! contract, KNOWN-HAZARDS 4.3), resolves the target under the D3c policy\n   *8│//! ([`resolve_across_visible`]: refuse-and-qualify on ambiguity, never a\n    9│//! guess), and ships one [`WanMessage`] over the broker's QUIC endpoint to\n   10│//! the resolved node. The receiving daemon's dispatcher funnels it through\n   11│//! the access gate + op-id dedup into the target's local delivery\n     │...\n   27│use spt_net::net::registry::{\n  *28│    key_prefix, resolve_across_visible, Ambiguity, Resolution, SubnetRegistry,\n   29│};\n   30│use spt_net::net::wanmsg::WanMessage;\n   31│use spt_proto::addr::Address;\n     │...\n  109│/// REQ-RC-CROSS-NODE-ATTACH). Mirrors the [`wan_send`]/[`wan_rest`] resolve-dial\n *110│/// discipline exactly (parse → [`resolve_across_visible`] → own-node degrade →\n  111│/// [`seed_first_addr`] → [`Brain::net_dial`]) so the attach leg shares the same\n  112│/// routing — and the same stale-tolerant, refuse-and-qualify semantics — the\n  113│/// message leg already proves. The `net_dial` rides the broker call deadline\n     │...\n  115│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n *116│pub fn resolve_and_dial_owner(brain: &mut Brain, endpoint: &str) -> OwnerDial {\n  117│    let own_hex = match spt_store::nodeid::load_or_create() {\n  118│        Ok(id) => id.public_key().to_hex(),\n  119│        Err(e) => {\n     │...\n  145│\n *146│    let instance = match resolve_across_visible(&regs, &address, &own_hex, excluded) {\n  147│        Resolution::Resolved { instance, .. } => instance,\n  148│        Resolution::Ambiguous(a) => return OwnerDial::Ambiguous(render_refusal(&address.id, &a, &regs)),\n  149│        Resolution::NotFound => return OwnerDial::NotFound,\n     │...\n  236│\n *237│    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {\n  238│        Resolution::Resolved { instance, .. } => instance,\n  239│        // Refusals render as valid copy-paste targets (REQ-INST-10), never a\n  240│        // debug dump the user can't act on.\n     │...\n  408│\n *409│    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {\n  410│        Resolution::Resolved { instance, .. } => instance,\n  411│        Resolution::Ambiguous(a) => {\n  412│            return WanRestOutcome::Ambiguous(render_refusal(&address.id, &a, regs))\n     │...\n  541│\n *542│    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {\n  543│        Resolution::Resolved { instance, .. } => instance,\n  544│        Resolution::Ambiguous(a) => {\n  545│            return WanShellLinkOutcome::Ambiguous(render_refusal(&address.id, &a, regs))\n     │...\n  929│        let (a_name, b_name) = (\n *930│            format!(\"spt-wansend-a-{}.sock\", std::process::id()),\n *931│            format!(\"spt-wansend-b-{}.sock\", std::process::id()),\n  932│        );\n  933│        let _a = net_broker(&a_name, dir.path());\n  934│        let _b = net_broker(&b_name, dir.path());\n     │...\n  946│            let registry = Arc::new(RegistryHost::new_at(\n *947│                \"b-wansend\",\n  948│                EpochSource::load_from(&dir.path().join(\"b-epoch.json\")),\n  949│                dir.path().join(\"b-registry\"),\n  950│            ));\n     │...\n  957│        let target = \"ling-wan\";\n *958│        let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n  959│        std::fs::create_dir_all(&perch_path).unwrap();\n  960│        let rec = InfoJson::new(target, \"now\", std::process::id(), \"s\", \"ready_agent\");\n  961│        info::write_info(&perch_path, &rec).unwrap();\n     │...\n 1084│        let target = \"ling-rest\";\n*1085│        let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n 1086│        std::fs::create_dir_all(&perch_path).unwrap();\n 1087│        let rec = InfoJson::new(target, \"now\", std::process::id(), \"s\", \"live_agent\");\n 1088│        info::write_info(&perch_path, &rec).unwrap();\n*1089│        spool::spool_message_deferred_at(&perch_path, \"psyche\", \"deferred brief\").unwrap();\n*1090│        spool::spool_message_at(&perch_path, \"bob\", \"live one\").unwrap();\n 1091│\n 1092│        let mut a = connect_retry(&a_name);\n 1093│        let mut b_probe = connect_retry(&b_name);\n     │...\n 1334│    /// Write a registry snapshot to the canonical on-disk path\n*1335│    /// [`resolve_and_dial_owner`] reads via [`load_snapshots`] — the production\n 1336│    /// disk path, not an injected map.\n 1337│    fn write_snapshot(subnet: &str, id: &str, node: &str) {\n 1338│        let dir = perch::identity_dir().join(\"registry\");\n     │...\n 1354│    #[test]\n*1355│    fn resolve_and_dial_owner_is_honest_on_resolution_misses() {\n 1356│        let _home = isolated_home();\n 1357│        let dir = tempfile::tempdir().unwrap();\n 1358│        let name = format!(\"spt-rcresolve-{}.sock\", std::process::id());\n     │...\n 1363│        assert!(matches!(\n*1364│            resolve_and_dial_owner(&mut a, \"ghost\"),\n 1365│            OwnerDial::NotFound\n 1366│        ));\n 1367│\n     │...\n 1374│        assert!(matches!(\n*1375│            resolve_and_dial_owner(&mut a, \"mine\"),\n 1376│            OwnerDial::NotFound\n 1377│        ));\n 1378│\n     │...\n 1382│        assert!(matches!(\n*1383│            resolve_and_dial_owner(&mut a, \"twin\"),\n 1384│            OwnerDial::Ambiguous(_)\n 1385│        ));\n 1386│    }\n\n## spt-msg/src/\n### deliver.rs\n   9│//! Routing path (the REQ-MSG-1 contract):\n *10│//! 1. `resolve_address` (stale-clean first) maps the target id -> a registered\n  11│//!    `127.0.0.1:<port>`.\n  12│//! 2. TCP `connect_timeout` + framed write. **Success is the only thing that\n  13│//!    counts as a live delivery.**\n    │...\n  68│pub fn perch_exists(target: &str) -> bool {\n *69│    perch::resolve_info_file(target, ParentHint::Infer).exists()\n  70│}\n  71│\n  72│/// True if `target` is online: its `ready` marker exists and the single liveness\n    │...\n  80│pub fn is_online(target: &str) -> bool {\n *81│    if !perch::resolve_ready_file(target, ParentHint::Infer).exists() {\n  82│        return false;\n  83│    }\n *84│    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n  85│    spt_store::liveness::is_perch_alive(&perch_path)\n  86│}\n  87│\n    │...\n  95│// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]\n *96│pub fn deliver_tcp(target: &str, from: &str, body: &str, owlery: &Path) -> bool {\n *97│    if let Some(addr_str) = registry::resolve_address(target, owlery) {\n  98│        if let Ok(addr) = addr_str.parse::<SocketAddr>() {\n  99│            match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {\n 100│                Ok(mut stream) => {\n    │...\n 131│    //    double-delivery).\n*132│    if deliver_tcp(target, from, body, owlery) {\n 133│        return DeliveryPath::Tcp;\n 134│    }\n 135│\n 136│    // 2. Spool fallback — the poll drain delivers it when the target starts.\n*137│    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n*138│    let _ = spool::spool_message_at(&perch_path, from, body);\n 139│    DeliveryPath::Spool\n 140│}\n 141│\n    │...\n 149│// [impl->REQ-MSG-1]\n*150│pub fn send(target: &str, from: &str, body: &str, owlery: &Path) -> SendOutcome {\n 151│    let trimmed = body.trim();\n 152│    if trimmed.is_empty() {\n 153│        return SendOutcome::Empty;\n    │...\n 174│// [impl->REQ-MSG-1]\n*175│pub fn send_deferred(target: &str, from: &str, body: &str) -> SendOutcome {\n 176│    let trimmed = body.trim();\n 177│    if trimmed.is_empty() {\n 178│        return SendOutcome::Empty;\n    │...\n 182│    }\n*183│    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n*184│    let _ = spool::spool_message_deferred_at(&perch_path, from, trimmed);\n 185│    SendOutcome::Queued\n 186│}\n 187│\n 188│/// Window-aware send (ADR-0028 W2). `window` is the delivery-window axis\n*189│/// (`default` | `idle_only` | `active_only`); `ephemeral` is the persistence axis\n 190│/// (carried on the row; evaporation wires in W3 with the channel axis).\n*191│/// - `active_only` is the hook channel — spool-only, NO live TCP/wake (= the\n 192│///   former `--deferred`).\n 193│/// - `default` / `idle_only` try a live TCP handoff first (the relay IS the idle\n 194│///   wake), then fall back to the spool TAGGED with the window so the drain\n    │...\n 199│// [impl->REQ-MSG-DELIVERY-AXES]\n*200│pub fn send_windowed(\n 201│    target: &str,\n 202│    from: &str,\n 203│    body: &str,\n    │...\n 214│    }\n*215│    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n*216│    // active_only never wakes — spool straight to the hook channel.\n*217│    if window != spool::WINDOW_ACTIVE_ONLY && deliver_tcp(target, from, trimmed, owlery) {\n 218│        return SendOutcome::Sent;\n 219│    }\n*220│    let _ = spool::spool_message_windowed_at(\n 221│        &perch_path,\n 222│        from,\n 223│        trimmed,\n### listener.rs\n 12│//! address survives a crashed listener — the recycled-pid guard\n*13│//! ([`spt_store::registry::resolve_address`]) is the second line of defense.\n 14│//!\n 15│//! **Interim model (M1):** one standalone listener per ready agent. M3's\n 16│//! `spt-daemon` consolidates all listeners into one supervisor; the surface is\n   │...\n 94│        let owlery = perch::owlery_dir();\n*95│        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 96│\n 97│        let listener = TcpListener::bind(\"127.0.0.1:0\")\n 98│            .map_err(|e| format!(\"Failed to bind TCP listener: {}\", e))?;\n### ready.rs\n  66│        spt_proto::id::validate_endpoint_id(id).map_err(|e| e.to_string())?;\n *67│        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n  68│        fs::create_dir_all(&perch_path)\n  69│            .map_err(|e| format!(\"Failed to create perch dir: {}\", e))?;\n  70│\n    │...\n 105│        // ready marker — the liveness signal senders check.\n*106│        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\n 107│            .map_err(|e| format!(\"Failed to write ready marker: {}\", e))?;\n 108│\n 109│        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind\n    │...\n 202│    pub fn soft_cleanup(&mut self) {\n*203│        let _ = fs::remove_file(perch::resolve_ready_file(&self.id, ParentHint::Infer));\n 204│        self.listener.close();\n 205│        self.closed = true;\n 206│    }\n    │...\n 266│        // info.json + ready marker on disk.\n*267│        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 268│        assert!(info::read_info(&perch_path).is_some());\n*269│        assert!(perch::resolve_ready_file(&id, ParentHint::Infer).exists());\n 270│        drop(agent);\n 271│    }\n 272│\n    │...\n 279│        // Pre-create an offline perch and spool a message to it.\n*280│        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 281│        fs::create_dir_all(&perch_path).unwrap();\n 282│        let rec = InfoJson::new(&id, \"now\", std::process::id(), \"sess\", \"ready_agent\");\n 283│        info::write_info(&perch_path, &rec).unwrap();\n    │...\n 329│        let (mut agent, _) = ReadyAgent::start(&id).unwrap();\n*330│        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 331│\n 332│        // A deferred row survives the startup drain — use it to prove the spool\n 333│        // is preserved across cleanup.\n*334│        spool::spool_message_deferred_at(&perch_path, \"hooker\", \"deferred body\").unwrap();\n 335│        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);\n 336│\n 337│        agent.soft_cleanup();\n    │...\n 339│        // ready marker gone...\n*340│        assert!(!perch::resolve_ready_file(&id, ParentHint::Infer).exists());\n 341│        // ...but spool row + info.json preserved.\n 342│        assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);\n 343│        assert!(info::read_info(&perch_path).is_some());\n    │...\n 355│        let (mut agent, _) = ReadyAgent::start(&id).unwrap();\n*356│        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 357│\n 358│        // The bind wrote exactly one Boot row keyed on the agent's session id.\n 359│        let rows = spt_store::sessions::read_all(&perch_path);\n### ring.rs\n  53│fn from_has_live_perch(from: &str) -> bool {\n *54│    if !perch::resolve_ready_file(from, ParentHint::Infer).exists() {\n  55│        return false;\n  56│    }\n *57│    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n  58│    spt_store::liveness::is_perch_alive(&perch_path)\n  59│}\n  60│\n    │...\n  98│    // Block for the reply: poll the listener and the spool until the deadline.\n *99│    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 100│    let deadline = Instant::now() + timeout;\n 101│    let outcome = loop {\n 102│        if let Some((from, body)) = reply_listener.check_message_blocking() {\n    │...\n 125│fn setup_ephemeral(from: &str) -> Result<PollListener, String> {\n*126│    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 127│    fs::create_dir_all(&perch_path).map_err(|e| format!(\"create ephemeral perch: {}\", e))?;\n 128│    let rec = InfoJson::new(from, \"once\", std::process::id(), \"ring\", \"ready_agent\");\n 129│    info::write_info(&perch_path, &rec).map_err(|e| format!(\"write info.json: {}\", e))?;\n*130│    fs::write(perch::resolve_ready_file(from, ParentHint::Infer), \"\")\n 131│        .map_err(|e| format!(\"write ready marker: {}\", e))?;\n 132│    PollListener::bind_blocking(from, REPLY_POLL)\n 133│}\n    │...\n 137│fn cleanup_ephemeral(from: &str) {\n*138│    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n*139│    let _ = fs::remove_file(perch::resolve_ready_file(from, ParentHint::Infer));\n*140│    let _ = fs::remove_file(perch::resolve_info_file(from, ParentHint::Infer));\n*141│    let _ = fs::remove_file(perch::resolve_spool_db(from, ParentHint::Infer));\n 142│    // Recursive: the wholly-owned ephemeral perch dir also carries the `.info.lock`\n 143│    // sentinel that write_info now creates (REQ-HAZARD-INFO-RMW-LOST-UPDATE); a\n 144│    // non-recursive remove_dir would leak the dir on that stray file (as every other\n    │...\n 162│    fn perch_gone(id: &str) -> bool {\n*163│        !perch::resolve_perch_path(id, ParentHint::Infer).exists()\n 164│    }\n 165│\n 166│    // [unit->REQ-MSG-1] ring delivers and returns the reply; ephemeral perch is\n    │...\n 233│        // Offline perch (exists, no listener).\n*234│        let perch_path = perch::resolve_perch_path(&target, ParentHint::Infer);\n 235│        fs::create_dir_all(&perch_path).unwrap();\n 236│        let rec = InfoJson::new(&target, \"now\", std::process::id(), \"sess\", \"ready_agent\");\n 237│        info::write_info(&perch_path, &rec).unwrap();\n\n## spt-daemon/src/\n### inject.rs\n   5│//! listen` relay (no advertised TCP address). These are exactly the endpoints a\n  *6│//! plain delivery would otherwise SILENTLY SPOOL — `deliver_tcp` finds no address\n   7│//! — so they must instead be reached by injecting the `<EVENT>` into the\n   8│//! broker-held PTY through the live translation binary.\n   9│//!\n    │...\n  12│//! (the cross-node WAN ingress) had NO such leg — it tried the relay\n *13│//! (`deliver_tcp`) then fell straight to spool, so a WAN arrival to an idle\n  14│//! spt-hosted perch with a healthy binary starved until an adapter hook happened\n  15│//! to poll (the \"sent but never lands\" report). This module is the ONE shared\n  16│//! implementation both paths call, so cross-node and local delivery cannot drift.\n    │...\n  31│/// broker-PTY-`controllable`. A harness-hosted endpoint with a relay\n *32│/// (`resolve_address` is `Some`) takes the normal TCP path; an offline or\n  33│/// non-controllable perch is not a PTY-inject target. Factored verbatim from\n  34│/// `cmd_send`'s predicate so the WAN ingress shares ONE definition.\n  35│// [impl->REQ-WAN-SPT-HOSTED-DELIVERY]\n    │...\n  40│    }\n *41│    if spt_store::registry::resolve_address(target, owlery).is_some() {\n  42│        return false; // a relay listener exists → normal TCP delivery\n  43│    }\n *44│    spt_store::info::read_info(&spt_store::perch::resolve_perch_path(\n  45│        target,\n  46│        spt_store::perch::ParentHint::Infer,\n  47│    ))\n    │...\n  74│// [impl->REQ-SEND-SPT-HOSTED]\n *75│pub fn try_spt_hosted_inject(\n  76│    target: &str,\n  77│    from: &str,\n  78│    body: &str,\n    │...\n  97│/// an operator poke. Identical mechanic to the F-023 idle-EDGE drain: CLAIM the pending\n *98│/// rows (audited, idle-inject leg), offer each through [`try_spt_hosted_inject`]\n  99│/// (`native=false` — the genuine idle window), and on the first miss (no live\n 100│/// translation binary, or the broker still views the endpoint ACTIVE) RELEASE the row\n 101│/// plus all not-yet-attempted back to the spool intact (v0.14.3 LAW: spooled loud, never\n    │...\n 112│    let perch_path =\n*113│        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);\n*114│    // Resting gate (REQ-INST-6 / KH 1.4): hold the deferred (`active_only`) rows while\n 115│    // the owner rests — the non-deferred set always drains on the idle window.\n 116│    let include_deferred = !crate::resting::deferred_held(&perch_path);\n 117│    // [impl->REQ-SPOOL-TAKE-AUDIT] idle-inject leg — stamp the claim's provenance.\n    │...\n 131│    for (msg_id, from, body) in rest.by_ref() {\n*132│        match try_spt_hosted_inject(id, &from, &body, owlery, false) {\n 133│            Some((true, _)) => delivered += 1,\n 134│            _ => {\n 135│                release_ids.push(msg_id);\n    │...\n 166│    let perch_path =\n*167│        spt_store::perch::resolve_perch_path(id, spt_store::perch::ParentHint::Infer);\n*168│    if !spt_store::perch::resolve_idle_file(id, spt_store::perch::ParentHint::Infer).exists() {\n 169│        return false;\n 170│    }\n 171│    spt_store::spool::pending_count_at(&perch_path).unwrap_or(0) > 0\n    │...\n 183│    fn establish_hosted(id: &str) -> std::path::PathBuf {\n*184│        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 185│        std::fs::create_dir_all(&perch_path).unwrap();\n 186│        let mut rec = InfoJson::new(id, \"0\", std::process::id(), \"sid\", \"live_agent\");\n 187│        rec.controllable = Some(true);\n 188│        info::write_info(&perch_path, &rec).unwrap();\n*189│        std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 190│        info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 191│        perch_path\n 192│    }\n    │...\n 270│            // Pending row but NOT idle → skip (the broker's active-gate owns *when*).\n*271│            spt_store::spool::spool_message_at(&perch_path, \"bob\", \"hi\").unwrap();\n 272│            assert!(\n 273│                !has_parked_idle_spool(id, &owlery),\n 274│                \"pending but not idle → the belt skips it\"\n    │...\n 276│            // Set the `.idle` sentinel → idle + hosted + pending = re-offer this tick.\n*277│            std::fs::write(perch::resolve_idle_file(id, ParentHint::Infer), \"\").unwrap();\n 278│            assert!(\n 279│                has_parked_idle_spool(id, &owlery),\n 280│                \"idle + hosted + pending → the belt re-offers\"\n\n## spt-net/src/net/\n### wanmsg.rs\n 112│/// resolution policy (local-first, refuse-ambiguous, `@node` pin, visibility)\n*113│/// already ran inside `resolve_across_visible`; this only splits its hit into\n 114│/// the local and wire legs.\n 115│// [impl->REQ-NET-1]\n 116│pub fn route_wan(resolution: Resolution, local_node: &str) -> WanRoute {"},"isError":false,"timestamp":1784197397368}}
{"type":"message","id":"bec2b18f","parentId":"dd1b94c0","timestamp":"2026-07-16T10:23:17.369Z","message":{"role":"toolResult","toolCallId":"call_8h4xVGKkpZHNyivZzthZEIX3|fc_017bff223290b7ba016a58b1155034819abb160fdcc709ba69","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/src/api/\n### startup.rs\n 117|/// Pure of any listener I/O so it is unit-testable without spawning processes.\n*118|/// Returns the perch's capability token on success.\n 119|///\n 120|/// REQ-START-3, refined by the F-034 leg-b split (doyle gate ruling, PR #80):\n 121|/// a seed is SPENT (stays taken) by a successful bind AND by any refusal that\n...\n 261|\n*262|/// Write the perch record (skeleton→live) + ready marker + capability token.\n*263|/// Reviving an existing perch keeps its token; a live perch held under a\n 264|/// different session id is a conflict (pid-recycle / id collision guard).\n 265|///\n 266|/// Creation-time fields ride the shared seam\n...\n 465|\n*466|    // Keep an existing token across a revive; mint one for a fresh perch.\n*467|    let token = auth::read_token(&perch_path).unwrap_or_else(auth::mint_token);\n*468|    auth::write_token(&perch_path, &token).map_err(|e| BindError::Io(e.to_string()))?;\n*469|    Ok(token)\n 470|}\n 471|\n 472|/// `api listen <id>` — consume the seed, bind the perch, then run the relay\n...\n 586|\n*587|    let token = match bind_from_seed(id, parent_pid, subnet, adapter_option.as_deref()) {\n 588|        Ok(t) => t,\n 589|        // Late go-live / post-daemon-restart (F-034 leg c): the ephemeral seed\n 590|        // is gone and nothing re-fires it until the NEXT SessionStart, so an\n...\n 613|    };\n*614|    eprintln!(\"BOUND:{id} token={token}\");\n 615|\n 616|    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 617|    let relay = match spt_daemon::relay::Relay::bind(id, &perch_path) {\n...\n 740|    ) {\n*741|        Ok(token) => {\n*742|            eprintln!(\"BOUND:{id} token={token}\");\n 743|            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)\n 744|            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen\n 745|            // mark. A live_agent endpoint whose manifest declares\n...\n 917|        put_seed(&reg, anchor, \"sid-1\");\n*918|        let token = bind_from_seed(\"alice\", anchor, None, None).expect(\"bind\");\n*919|        assert_eq!(token.len(), 32);\n 920|\n 921|        let perch_path = perch::resolve_perch_path(\"alice\", ParentHint::Infer);\n 922|        let rec = info::read_info(&perch_path).expect(\"info.json\");\n...\n 1339|\n*1340|        let token = establish_perch(\n 1341|            \"attacker\",\n 1342|            \"psid-unrelated\",\n 1343|            std::process::id(),\n...\n 1350|        .expect(\"an unrelated sid binds normally\");\n*1351|        assert_eq!(token.len(), 32);\n 1352|        let rec = info::read_info(&perch::resolve_perch_path(\"attacker\", ParentHint::Infer))\n 1353|            .expect(\"attacker perch bound\");\n 1354|        assert_eq!(rec.session_id, \"psid-unrelated\");\n...\n 1469|        let anchor = std::process::id();\n*1470|        let token = bind_from_session_id(\"late\", \"sid-late\", anchor, None, None)\n 1471|            .expect(\"alive pid + fresh sid binds\");\n*1472|        assert_eq!(token.len(), 32);\n 1473|\n 1474|        let rec = info::read_info(&perch::resolve_perch_path(\"late\", ParentHint::Infer))\n 1475|            .expect(\"perch info.json after sid-fallback bind\");\n...\n 1500|        // The fallback then binds from the sid — the pair cmd_listen composes.\n*1501|        let token = bind_from_session_id(\"late\", \"sid-late\", anchor, None, None)\n 1502|            .expect(\"sid fallback binds after NoSeed\");\n*1503|        assert_eq!(token.len(), 32);\n 1504|        assert_eq!(\n 1505|            info::read_info(&perch::resolve_perch_path(\"late\", ParentHint::Infer))\n 1506|                .unwrap()\n### delivery.rs\n 166|\n*167|/// `api poll <shell-id> --link <token>` — the **shell relay drain** (M5-D3c,\n*168|/// CONTEXT §Shell model: child-relay command receipt). The link token is the\n 169|/// auth (\"owner from the link\" — the binary knows nothing else); the drained\n 170|/// rows are the MAC-stamped command/text/file frames the owner spooled, one\n*171|/// per line. A token that resolves no instance, or one belonging to a\n 172|/// different instance than named, refuses.\n 173|///\n 174|/// **SCOPE CARVE-OUT (ADR-0020): this surface is NOT an arriving-message\n...\n 188|    let owlery = perch::owlery_dir();\n*189|    let Some((owner, shell_id)) = spt_daemon::shellhost::find_shell_by_token(&owlery, link) else {\n*190|        eprintln!(\"AUTH_REFUSED:{id}: no instance holds this link token\");\n 191|        return EXIT_REFUSED;\n 192|    };\n 193|    if shell_id != id {\n*194|        eprintln!(\"AUTH_REFUSED:{id}: the link token belongs to {shell_id}\");\n 195|        return EXIT_REFUSED;\n 196|    }\n 197|    let perch_path = perch::resolve_shell_perch_path_in(&owlery, &owner, &shell_id);\n\n## spt-msg/src/\n### lib.rs\n 10|//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the\n*11|//! mis-elevated legacy reply-to framing). The delivery routing, listener\n 12|//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against\n 13|//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).\n 14|//!\n### wire.rs\n 3|//! The frame carries `(from, body)` **structurally** — there is no\n*4|//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated\n 5|//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in\n 6|//! [`spt_proto::event`], composed once at the delivery edge, never on the wire).\n 7|//! spt-core mandates **no** byte-for-byte TCP interop with any sister `spt`."}],"details":{"scopePath":"crates/spt-store/src/info.rs, crates/spt-store/src/registry.rs, crates/spt-net/src/net/registry.rs, crates/spt/src/api/startup.rs, crates/spt/src/api/delivery.rs, crates/spt-msg/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":27,"fileCount":4,"files":["crates/spt/src/api/startup.rs","crates/spt/src/api/delivery.rs","crates/spt-msg/src/lib.rs","crates/spt-msg/src/wire.rs"],"fileMatches":[{"path":"crates/spt/src/api/startup.rs","count":19},{"path":"crates/spt/src/api/delivery.rs","count":6},{"path":"crates/spt-msg/src/lib.rs","count":1},{"path":"crates/spt-msg/src/wire.rs","count":1}],"truncated":false,"displayContent":"# crates/\n\n## spt/src/api/\n### startup.rs\n  117│/// Pure of any listener I/O so it is unit-testable without spawning processes.\n *118│/// Returns the perch's capability token on success.\n  119│///\n  120│/// REQ-START-3, refined by the F-034 leg-b split (doyle gate ruling, PR #80):\n  121│/// a seed is SPENT (stays taken) by a successful bind AND by any refusal that\n     │...\n  261│\n *262│/// Write the perch record (skeleton→live) + ready marker + capability token.\n *263│/// Reviving an existing perch keeps its token; a live perch held under a\n  264│/// different session id is a conflict (pid-recycle / id collision guard).\n  265│///\n  266│/// Creation-time fields ride the shared seam\n     │...\n  465│\n *466│    // Keep an existing token across a revive; mint one for a fresh perch.\n *467│    let token = auth::read_token(&perch_path).unwrap_or_else(auth::mint_token);\n *468│    auth::write_token(&perch_path, &token).map_err(|e| BindError::Io(e.to_string()))?;\n *469│    Ok(token)\n  470│}\n  471│\n  472│/// `api listen <id>` — consume the seed, bind the perch, then run the relay\n     │...\n  586│\n *587│    let token = match bind_from_seed(id, parent_pid, subnet, adapter_option.as_deref()) {\n  588│        Ok(t) => t,\n  589│        // Late go-live / post-daemon-restart (F-034 leg c): the ephemeral seed\n  590│        // is gone and nothing re-fires it until the NEXT SessionStart, so an\n     │...\n  613│    };\n *614│    eprintln!(\"BOUND:{id} token={token}\");\n  615│\n  616│    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n  617│    let relay = match spt_daemon::relay::Relay::bind(id, &perch_path) {\n     │...\n  740│    ) {\n *741│        Ok(token) => {\n *742│            eprintln!(\"BOUND:{id} token={token}\");\n  743│            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)\n  744│            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen\n  745│            // mark. A live_agent endpoint whose manifest declares\n     │...\n  917│        put_seed(&reg, anchor, \"sid-1\");\n *918│        let token = bind_from_seed(\"alice\", anchor, None, None).expect(\"bind\");\n *919│        assert_eq!(token.len(), 32);\n  920│\n  921│        let perch_path = perch::resolve_perch_path(\"alice\", ParentHint::Infer);\n  922│        let rec = info::read_info(&perch_path).expect(\"info.json\");\n     │...\n 1339│\n*1340│        let token = establish_perch(\n 1341│            \"attacker\",\n 1342│            \"psid-unrelated\",\n 1343│            std::process::id(),\n     │...\n 1350│        .expect(\"an unrelated sid binds normally\");\n*1351│        assert_eq!(token.len(), 32);\n 1352│        let rec = info::read_info(&perch::resolve_perch_path(\"attacker\", ParentHint::Infer))\n 1353│            .expect(\"attacker perch bound\");\n 1354│        assert_eq!(rec.session_id, \"psid-unrelated\");\n     │...\n 1469│        let anchor = std::process::id();\n*1470│        let token = bind_from_session_id(\"late\", \"sid-late\", anchor, None, None)\n 1471│            .expect(\"alive pid + fresh sid binds\");\n*1472│        assert_eq!(token.len(), 32);\n 1473│\n 1474│        let rec = info::read_info(&perch::resolve_perch_path(\"late\", ParentHint::Infer))\n 1475│            .expect(\"perch info.json after sid-fallback bind\");\n     │...\n 1500│        // The fallback then binds from the sid — the pair cmd_listen composes.\n*1501│        let token = bind_from_session_id(\"late\", \"sid-late\", anchor, None, None)\n 1502│            .expect(\"sid fallback binds after NoSeed\");\n*1503│        assert_eq!(token.len(), 32);\n 1504│        assert_eq!(\n 1505│            info::read_info(&perch::resolve_perch_path(\"late\", ParentHint::Infer))\n 1506│                .unwrap()\n### delivery.rs\n 166│\n*167│/// `api poll <shell-id> --link <token>` — the **shell relay drain** (M5-D3c,\n*168│/// CONTEXT §Shell model: child-relay command receipt). The link token is the\n 169│/// auth (\"owner from the link\" — the binary knows nothing else); the drained\n 170│/// rows are the MAC-stamped command/text/file frames the owner spooled, one\n*171│/// per line. A token that resolves no instance, or one belonging to a\n 172│/// different instance than named, refuses.\n 173│///\n 174│/// **SCOPE CARVE-OUT (ADR-0020): this surface is NOT an arriving-message\n    │...\n 188│    let owlery = perch::owlery_dir();\n*189│    let Some((owner, shell_id)) = spt_daemon::shellhost::find_shell_by_token(&owlery, link) else {\n*190│        eprintln!(\"AUTH_REFUSED:{id}: no instance holds this link token\");\n 191│        return EXIT_REFUSED;\n 192│    };\n 193│    if shell_id != id {\n*194│        eprintln!(\"AUTH_REFUSED:{id}: the link token belongs to {shell_id}\");\n 195│        return EXIT_REFUSED;\n 196│    }\n 197│    let perch_path = perch::resolve_shell_perch_path_in(&owlery, &owner, &shell_id);\n\n## spt-msg/src/\n### lib.rs\n 10│//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the\n*11│//! mis-elevated legacy reply-to framing). The delivery routing, listener\n 12│//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against\n 13│//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).\n 14│//!\n### wire.rs\n 3│//! The frame carries `(from, body)` **structurally** — there is no\n*4│//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated\n 5│//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in\n 6│//! [`spt_proto::event`], composed once at the delivery edge, never on the wire).\n 7│//! spt-core mandates **no** byte-for-byte TCP interop with any sister `spt`."},"isError":false,"timestamp":1784197397369}}
{"type":"message","id":"028a72ea","parentId":"bec2b18f","timestamp":"2026-07-16T10:23:17.378Z","message":{"role":"toolResult","toolCallId":"call_KWGZVibikAv0Sk4ecqZl1Qo0|fc_017bff223290b7ba016a58b1155044819a8b8cb9360ebca3bf","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/src/api/\n### auth.rs\n 32|\n*33|/// Persist a capability token into an (already-created) perch dir.\n 34|pub fn write_token(perch_path: &Path, token: &str) -> std::io::Result<()> {\n 35|    std::fs::write(perch_path.join(TOKEN_FILE), token)\n 36|}\n...\n 97|        // which is how a live session would re-pin — AUTH_REFUSES forever (a silent,\n*98|        // permanent strand: ready:false, stale .idle, drains no-op, WAN spool sleeps).\n 99|        // Accept the caller's sid and RE-PIN. Same trust model as establish_perch's\n 100|        // dead-owner rebind (an orphaned perch accepts a new LOCAL owner exactly when\n 101|        // owner_alive == false). ADDITIVE to token auth: fires ONLY on (no/failed\n...\n 266|            &path,\n*267|            &InfoJson::new(id, \"2026-06-01T00:00:00Z\", pid, session_id, \"ready_agent\"),\n 268|        )\n 269|        .unwrap();\n 270|        let token = mint_token();\n### delivery.rs\n 181|/// everywhere rule (ADR-0020) covers only the harness arriving-message surfaces\n*182|/// on AGENT perches (`api listen`/`spt ready`/`api poll`/`api worker-poll`), not\n 183|/// this internal shell-command transport. Each row's body rides RAW, one per\n 184|/// line. **`notify_shell_e2e` is the regression guard — do not \"fix\" this back\n 185|/// into an `<EVENT>` wrap.**\n...\n 253|    // The claim/offer/release/evaporate core is the ONE shared `drain_idle_spool`\n*254|    // (spt_daemon::inject) — the same leg the send-time already-idle path and the\n 255|    // pulse-tick re-offer belt (REQ-IDLE-PARKED-DELIVERY) call, so the idle-EDGE drain\n 256|    // and the parked-idle drain cannot drift.\n 257|    let owlery = perch::owlery_dir();\n### live.rs\n 7|//! brain now runs it as scheduled in-process loops paced by a durable config.\n*8|//! This module keeps the binary's call sites stable — `cmd_listen` /\n 9|//! `cmd_shutdown` — by forwarding to `BrainLifecycle`. The logic lives once, in\n 10|//! the daemon crate; the duplicate here is gone.\n 11|//\n...\n 49|/// host from the manifest and runs the echo-before-teardown ordering (T6). A\n*50|/// non-live manifest (no Psyche) is a plain soft-stop: just drop the `ready`\n 51|/// marker. Auth-gated by the dispatcher.\n 52|pub fn cmd_shutdown(id: &str, manifest: Option<&Manifest>, install_dir: Option<&Path>) -> i32 {\n 53|    let session_id = perch_session_id(id);\n...\n 69|            // Non-live endpoint: soft-stop only (remove the online marker).\n*70|            let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n 71|            eprintln!(\"SHUTDOWN:{id} (non-live soft-stop)\");\n 72|            0\n 73|        }\n### mod.rs\n 7|//! perch/spool/registry directly via spt-store/spt-msg, mirroring M1's\n*8|//! standalone listener. M3's `spt-daemon` swaps the on-disk seed + standalone\n 9|//! `listen` for an in-memory daemon record — the surface here stays thin so\n 10|//! that is a move, not a rewrite.\n 11|//!\n...\n 373|            session_id,\n*374|        } => startup::cmd_listen(\n 375|            &id,\n 376|            parent_pid,\n 377|            once,\n...\n 423|            let adapter = ctx.adapter.as_deref().filter(|a| !a.is_empty());\n*424|            startup::cmd_bind(\n 425|                &id,\n 426|                bind_session,\n 427|                auth.session_id,\n...\n 506|        // Read-only JSON report; the bare form self-resolves like whoami, so it takes\n*507|        // no auth (no mutation, exposes only what `endpoint list` already shows).\n 508|        ApiCmd::EndpointInfo { id } => reporting::cmd_endpoint_info(id.as_deref()),\n 509|        ApiCmd::HistoryLog { id, auth } => gated(&id, &auth, reporting::cmd_history_log),\n 510|        ApiCmd::DigestEntry { id, auth } => gated(&id, &auth, reporting::cmd_digest_entry),\n...\n 600|/// advertisement — hostable_types, `[env]`, `[session.notif]` (#5) — reflects\n*601|/// the profile. Returns an exit code on failure (already reported to stderr).\n 602|fn load_manifest(path: &PathBuf, adapter: &str) -> Result<Manifest, i32> {\n 603|    let text = std::fs::read_to_string(path).map_err(|e| {\n 604|        eprintln!(\"MANIFEST_READ:{}: {e}\", path.display());\n### reporting.rs\n 93|    // that instant, so the adapter's soft `api session-end` AUTHENTICATES and removes\n*94|    // the ready marker (cmd_session_end:206). The boundary then rotates the sid but\n*95|    // nothing re-writes ready → the perch reads is_online=false → try_spt_hosted_inject\n 96|    // Nones out on the CLI gate before any broker RPC → every post-clear force-native\n 97|    // (INCLUDING the checkpoint FIRE) reports the generic UNDELIVERED, FOREVER (SEAM-2,\n 98|    // B6's second half — the live-wake blocker). A boundary PROVES a live successor\n...\n 103|    // [impl->REQ-HAZARD-BOUNDARY-READY-STRAND]\n*104|    let _ = std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\");\n 105|    let _ = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE);\n 106|    // Append the rotated session to the ledger (REQ-TERM-6) so the digest spans\n 107|    // this boundary — enumerating the last K sessions, not just the new one. An\n...\n 161|    // one **deferred** shell-context row — hook-channel background context\n*162|    // that never wakes a listener (the live event-stream drain skips\n 163|    // deferred rows by design). Empty discovery (no instances, nothing\n 164|    // instantiable) injects nothing; best-effort like the notifs — a context\n 165|    // problem must never fail the boundary itself.\n...\n 252|    if !erase {\n*253|        // Soft (6.2): remove the ready marker; preserve spool/info.\n*254|        let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n 255|        // F-2 (REQ-SOFT-END-PRESERVES-LIVE-LISTENER): the relay address row is\n*256|        // LISTENER-scoped truth, not session-scoped — the poll listener SURVIVES\n 257|        // a /clear (whose adapter hook fires this soft end for the DEPARTING\n*258|        // session), and only the listener process knows its socket addr, so\n 259|        // deleting a live owner's row severs delivery unrecoverably (post-clear\n 260|        // sends hit NO_PERCH while the C-2 boundary re-stamp reads healthy).\n 261|        // Unregister ONLY a row whose owner is dead/offline per the ONE liveness\n...\n 268|        if !row_alive {\n*269|            let _ = registry::unregister_address(id, &owlery);\n 270|        }\n 271|        eprintln!(\"SESSION_END:{id} (soft; spool + history preserved)\");\n 272|        return 0;\n...\n 274|    // Erase: first GC the parent's REAPABLE workers (drained soft-stops + any\n*275|    // TTL-expired stale ready rows) so a leaked/stale worker never blocks the erase;\n 276|    // THEN cascade-guard only a GENUINELY-live remainder (REQ-WORKER-REAP + 6.3). The\n*277|    // parent is alive right now (we are erasing it), so the reap keeps a fresh ready\n 278|    // worker (in-flight) — which then correctly trips the cascade guard below.\n 279|    // [impl->REQ-WORKER-REAP]\n 280|    let _ = spt_store::worker_reap::reap_workers(\n...\n 289|    if !live.is_empty() {\n*290|        let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n 291|        eprintln!(\n 292|            \"CASCADE_GUARD:{id} hosts {} live child perch(es) — soft-cleaned, not erased:\",\n 293|            live.len()\n...\n 300|    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n*301|    let _ = registry::unregister_address(id, &owlery);\n 302|    if let Err(e) = std::fs::remove_dir_all(&perch_path) {\n 303|        eprintln!(\"SESSION_END_FAIL:{id}: {e}\");\n 304|        return EXIT_REFUSED;\n...\n 477|    let frame = spt_daemon::shellchan::compose_sensory_frame(&shell_id, sensory_type, payload);\n*478|    // Live-or-drop: TCP to the owner's live listener is the only delivery.\n 479|    if spt_msg::deliver::is_online(&owner)\n*480|        && spt_msg::deliver::deliver_tcp(&owner, \"\", &frame, &owlery)\n 481|    {\n 482|        eprintln!(\"SENSORY_DELIVERED:{shell_id} -> {owner} type={sensory_type}\");\n 483|    } else {\n...\n 655|        Ok(None) => {\n*656|            eprintln!(\"OWNER_SHUTDOWN_NOOP:{owner} (already resting)\");\n 657|            0\n 658|        }\n 659|        Err(e) => {\n...\n 790|/// has not yet seen; on a hit, mark it seen (durably) and return the formatted\n*791|/// line. Already-seen hits are skipped so a repeated keyword never permanently\n 792|/// blocks a later, distinct hint.\n 793|// [impl->REQ-MANIFEST-4]\n 794|fn select_and_mark_hint(\n...\n 807|        if seen.contains(&key) {\n*808|            continue; // already fired this session\n 809|        }\n 810|        if let Some(kw) = hint.matched_keyword(message) {\n 811|            let line = format!(\n...\n 1011|        info::write_info(&path, &rec).unwrap();\n*1012|        std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 1013|    }\n 1014|\n 1015|    // [unit->REQ-API-ENDPOINT-INFO] #7: attached_node is derived from the HONEST\n...\n 1192|            // shell-context row (M5-D3d agent awareness): the live event-stream\n*1193|            // drain skips it — hook-channel background context, never a listener\n 1194|            // wake — while a hook drain delivers it; empty discovery injects\n 1195|            // nothing at all.\n 1196|    fn boundary_injects_deferred_shell_context() {\n### startup.rs\n 96|    EmptySession,\n*97|    /// A *live* perch already holds this id under a different session (5.1).\n 98|    Conflict {\n 99|        id: String,\n 100|        held: String,\n...\n 116|/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.\n*117|/// Pure of any listener I/O so it is unit-testable without spawning processes.\n 118|/// Returns the perch's capability token on success.\n 119|///\n 120|/// REQ-START-3, refined by the F-034 leg-b split (doyle gate ruling, PR #80):\n...\n 216|/// live sid can revive that perch). Accepted local-surface posture: the\n*217|/// `--parent-pid` override already permits a local squat, so this stays a\n 218|/// doc+test qualification, not a redesign. The gates themselves hold exactly\n 219|/// as on a seeded bind: the parent-pid anchor must be ALIVE (5.1 pid-recycle\n 220|/// defense), `establish_perch` refuses a live-owner conflict, and the\n...\n 261|\n*262|/// Write the perch record (skeleton→live) + ready marker + capability token.\n 263|/// Reviving an existing perch keeps its token; a live perch held under a\n 264|/// different session id is a conflict (pid-recycle / id collision guard).\n 265|///\n...\n 277|#[allow(clippy::too_many_arguments)]\n*278|fn establish_perch(\n 279|    id: &str,\n 280|    session_id: &str,\n 281|    parent_pid: u32,\n...\n 382|        // Previously NEVER set on bind — the refuted v0.12.1 \"fresh agent shows under\n*383|        // its project\" P1. spt-hosted: cmd_bind passes its own current_dir (the broker\n 384|        // spawned it in project_cwd); harness-hosted: bind_from_seed passes the seed's\n 385|        // captured cwd. A revive carries the prior cwd forward (a re-bind must not wipe\n 386|        // it), like home/adapter/controllable.\n...\n 429|    };\n*430|    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\n 431|        .map_err(|e| BindError::Io(e.to_string()))?;\n 432|\n 433|    // Session ledger (REQ-TERM-6): record this session as active so the digest\n...\n 486|/// one code path both platforms; OS-native immediacy — Win job object / Linux PDEATHSIG\n*487|/// — is an OPTIONAL follow-up). The listener exits within one of these windows.\n 488|const PARENT_WATCH_POLL: std::time::Duration = std::time::Duration::from_secs(2);\n 489|\n*490|/// Process exit code when the listener self-exits because its `--parent-pid` host died\n 491|/// (REQ-HAZARD-LISTEN-ORPHAN) — distinct from a clean `0` or a refusal, so a log/operator\n 492|/// can tell an orphan-exit from a normal end.\n 493|const EXIT_PARENT_GONE: i32 = 3;\n 494|\n*495|/// One watchdog-tick decision (REQ-HAZARD-LISTEN-ORPHAN): the listener must exit once its\n 496|/// `--parent-pid` anchor is no longer alive. Pure over [`proc::is_process_alive`] so the\n 497|/// exit trigger is unit-testable without a real host fork.\n 498|// [impl->REQ-HAZARD-LISTEN-ORPHAN]\n...\n 502|\n*503|/// The loud orphan-exit line — names this listener + the dead parent, so a false-ONLINE\n 504|/// perch becomes a diagnosable exit, never a silent hang. Pure for the unit.\n 505|// [impl->REQ-HAZARD-LISTEN-ORPHAN]\n 506|fn listen_orphan_exit_line(id: &str, parent_pid: u32) -> String {\n...\n 508|        \"LISTEN_ORPHAN_EXIT:{id}: parent pid {parent_pid} is gone — exiting the orphaned \\\n*509|         listener so the perch flips OFFLINE (its recorded pid dies) and dead-owner rebind \\\n 510|         unblocks [REQ-HAZARD-LISTEN-ORPHAN]\"\n 511|    )\n 512|}\n...\n 514|/// Spawn the listen-orphan watchdog (REQ-HAZARD-LISTEN-ORPHAN). `--parent-pid` is the\n*515|/// auth anchor, but with no liveness watch a host death orphans the listener forever: the\n 516|/// perch stays falsely ONLINE, EVENTs stream to a dead stdout, and dead-owner rebind is\n 517|/// BLOCKED (the recorded pid is the live orphan). This bounded poll exits the process LOUD\n 518|/// within one [`PARENT_WATCH_POLL`] window on parent death, so the perch's recorded pid\n...\n 533|\n*534|pub fn cmd_listen(\n 535|    id: &str,\n 536|    parent_pid_override: Option<u32>,\n 537|    once: bool,\n...\n 616|    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n*617|    let relay = match spt_daemon::relay::Relay::bind(id, &perch_path) {\n 618|        Ok(r) => r,\n 619|        Err(e) => {\n 620|            eprintln!(\"LISTEN_FAIL:{id}: {e}\");\n...\n 670|    // Listen-orphan watchdog (REQ-HAZARD-LISTEN-ORPHAN): watch the `--parent-pid` host\n*671|    // and exit LOUD if it dies, so this listener never orphans the perch falsely ONLINE\n 672|    // (EVENTs to a dead stdout, dead-owner rebind blocked). Persistent branch only.\n 673|    // [impl->REQ-HAZARD-LISTEN-ORPHAN]\n 674|    spawn_parent_watchdog(id, parent_pid);\n...\n 691|// [impl->REQ-SEAM-POSTSPAWN]\n*692|pub fn cmd_bind(\n 693|    id: &str,\n 694|    set_session_id: Option<String>,\n 695|    proof_session: Option<String>,\n...\n 743|            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)\n*744|            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen\n 745|            // mark. A live_agent endpoint whose manifest declares\n 746|            // `[session.psyche_init]` is marked status=online at establish; the\n 747|            // brain's livehost reconcile then hosts the Psyche (the SAME single host\n 748|            // path — bind never spawns/hosts/calls livehost). Closes the\n*749|            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so\n 750|            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct\n 751|            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so\n 752|            // the establish-online signal == the reconcile host predicate (no\n...\n 791|\n*792|/// Emit one delivery to the listener stream: compose the structural\n 793|/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass\n 794|/// through; oversized lines chunk into EVENT-PART) and print + flush. One\n 795|/// delivery per emit.\n### worker.rs\n 10|//! A destructive parent teardown must never hard-delete a parent that still\n*11|//! hosts **live** children. \"Live\" here = the child still carries its `ready`\n 12|//! marker (the soft-cleanup signal; `worker-stop` removes it). When live\n 13|//! children exist, the caller soft-cleans and surfaces their paths instead of\n 14|//! recursively deleting in-flight work. [`live_child_paths`] is the guard\n...\n 94|    }\n*95|    // ready marker = the live-child signal the cascade-wipe guard checks.\n 96|    let _ = std::fs::write(\n*97|        perch::resolve_ready_file(&id, ParentHint::Explicit(parent)),\n 98|        \"\",\n 99|    );\n 100|    // STDOUT: the machine-readable result (bare minted id). STDERR: the human line.\n...\n 106|/// `api worker-stop <id>` — soft-clean a worker (REQ-HAZARD-SOFT-CLEANUP):\n*107|/// remove its `ready` marker so it no longer counts as a live child; the\n 108|/// `info.json` + spool are preserved. Idempotent.\n 109|// [impl->REQ-API-2]\n 110|pub fn cmd_worker_stop(id: &str) -> i32 {\n*111|    let removed = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer)).is_ok();\n 112|    if removed {\n 113|        eprintln!(\"WORKER_STOPPED:{id}\");\n 114|    } else {\n*115|        eprintln!(\"WORKER_STOPPED:{id} (no ready marker)\");\n 116|    }\n 117|    0\n 118|}\n...\n 145|/// The paths of `parent_id`'s **live** nested children (those still carrying a\n*146|/// `ready` marker). Empty ⇒ the parent is safe to hard-delete.\n 147|// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]\n 148|pub fn live_child_paths(parent_id: &str) -> Vec<std::path::PathBuf> {\n 149|    let nested = perch::resolve_perch_path(parent_id, ParentHint::Infer).join(\"nested\");\n...\n 158|        }\n*159|        // A child is live while its ready marker exists (info.json present =\n*160|        // it's a real perch; ready = not yet soft-cleaned).\n*161|        if info::read_info(&child).is_some() && child.join(\"ready\").exists() {\n 162|            live.push(child);\n 163|        }\n 164|    }\n...\n 199|        assert!(expected.join(\"info.json\").exists());\n*200|        assert!(expected.join(\"ready\").exists());\n 201|        // Inferred resolution agrees with the explicit nesting.\n 202|        assert_eq!(\n 203|            perch::resolve_perch_path(\"alice-w1\", ParentHint::Infer),\n...\n 250|            &ppath,\n*251|            &InfoJson::new(\"alice\", \"0\", std::process::id(), \"\", \"ready_agent\"),\n 252|        )\n 253|        .unwrap();\n 254|        assert_eq!(cmd_worker_start(\"alice\", None, None, Some(\"presented-sid\")), 0);\n...\n 275|\n*276|        // Soft-stop the worker → ready marker gone → no longer a live child.\n 277|        assert_eq!(cmd_worker_stop(\"alice-w1\"), 0);\n 278|        assert!(\n 279|            live_child_paths(\"alice\").is_empty(),\n...\n 281|        );\n*282|        // The record is preserved (soft cleanup), only the ready marker removed.\n 283|        let child = perch::resolve_perch_path(\"alice-w1\", ParentHint::Infer);\n 284|        assert!(child.join(\"info.json\").exists());\n*285|        assert!(!child.join(\"ready\").exists());\n 286|    }\n 287|\n 288|    #[test] // [unit->REQ-API-2] worker-poll drains the worker's own spool.\n\n## spt-msg/src/\n### deliver.rs\n 17|//! Two hazards ride on step 2/3:\n*18|//! - **Windows pid-recycle (5.1):** a dead listener's pid can be recycled to an\n 19|//!   unrelated live process, so a \"pid alive\" check alone can't prove the perch\n 20|//!   is real. The structural guard is that *connect must succeed* — a recycled\n 21|//!   process is not listening on the old ephemeral port, so the connect refuses\n...\n 46|pub enum DeliveryPath {\n*47|    /// Delivered live over TCP to the target's listener.\n 48|    Tcp,\n 49|    /// Target unreachable — written to the target's spool for later drain.\n 50|    Spool,\n...\n 71|\n*72|/// True if `target` is online: its `ready` marker exists and the single liveness\n 73|/// resolver ([`spt_store::liveness::is_perch_alive`]) reports it live. That\n 74|/// resolver is daemon-authoritative for daemon-hosted perches (their `status`\n 75|/// field) and per-pid for external/interim perches (a `BUSY`/unparseable pid with\n*76|/// a present `ready` marker reads alive — the listener is up between polls). The\n 77|/// per-pid assumption is **not** duplicated here (2.5) — it lives only in the\n 78|/// resolver, so the daemon swap stays localized.\n 79|// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n 80|pub fn is_online(target: &str) -> bool {\n*81|    if !perch::resolve_ready_file(target, ParentHint::Infer).exists() {\n 82|        return false;\n 83|    }\n 84|    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n...\n 95|// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]\n*96|pub fn deliver_tcp(target: &str, from: &str, body: &str, owlery: &Path) -> bool {\n 97|    if let Some(addr_str) = registry::resolve_address(target, owlery) {\n 98|        if let Ok(addr) = addr_str.parse::<SocketAddr>() {\n 99|            match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {\n...\n 109|                    // is \"alive\" (possibly recycled to an unrelated process), the\n*110|                    // refused connect already prevented misdelivery — the caller\n 111|                    // falls back.\n 112|                    if let Some(pid) = registry::lookup_pid(target, owlery) {\n 113|                        if !proc::is_process_alive(pid) {\n*114|                            let _ = registry::unregister_address(target, owlery);\n 115|                        }\n 116|                    }\n 117|                }\n...\n 131|    //    double-delivery).\n*132|    if deliver_tcp(target, from, body, owlery) {\n 133|        return DeliveryPath::Tcp;\n 134|    }\n 135|\n...\n 165|\n*166|/// Deferred send (the hook channel): spool-only, **no TCP, no listener wake**.\n 167|/// The row is tagged `deferred=1` so the live event-stream drain\n 168|/// (`drain_non_deferred_at`) skips it — it surfaces only when a hook consumer\n 169|/// drains everything (`drain_all_at` / `peek_all_at`) on its natural cadence.\n...\n 216|    // active_only never wakes — spool straight to the hook channel.\n*217|    if window != spool::WINDOW_ACTIVE_ONLY && deliver_tcp(target, from, trimmed, owlery) {\n 218|        return SendOutcome::Sent;\n 219|    }\n 220|    let _ = spool::spool_message_windowed_at(\n...\n 233|    use super::*;\n*234|    use crate::listener::PollListener;\n 235|    use spt_store::info;\n 236|    use std::net::SocketAddr;\n 237|\n...\n 253|        let id = unique_id(\"test-online\");\n*254|        let listener = PollListener::bind(&id).unwrap();\n 255|\n 256|        let path = deliver(&id, \"sender\", \"live msg\", &owlery);\n 257|        assert_eq!(path, DeliveryPath::Tcp);\n...\n 261|        for _ in 0..200 {\n*262|            if let Some(m) = listener.check_message() {\n 263|                got = Some(m);\n 264|                break;\n 265|            }\n...\n 293|        info::write_info(&perch_path, &rec).unwrap();\n*294|        let listener = PollListener::bind(&id).unwrap();\n 295|\n 296|        let outcome =\n 297|            send_windowed(&id, \"peer\", \"background ctx\", &owlery, spool::WINDOW_ACTIVE_ONLY, false);\n...\n 306|            assert!(\n*307|                listener.check_message().is_none(),\n 308|                \"active_only must never reach a relay's live poll channel (never-wake)\"\n 309|            );\n 310|            std::thread::sleep(Duration::from_millis(5));\n...\n 319|\n*320|    // [unit->REQ-MSG-1] offline target (perch exists, no listener) spools.\n 321|    #[test]\n 322|    fn offline_existing_perch_spools() {\n 323|        crate::test_home::isolate();\n...\n 326|        // Create the perch dir + a minimal info.json so perch_exists is true,\n*327|        // but bind no listener (offline).\n 328|        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 329|        std::fs::create_dir_all(&perch_path).unwrap();\n*330|        let rec = info::InfoJson::new(&id, \"now\", std::process::id(), \"sess\", \"ready_agent\");\n 331|        info::write_info(&perch_path, &rec).unwrap();\n 332|\n 333|        let path = deliver(&id, \"sender\", \"queued msg\", &owlery);\n...\n 338|    // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a registry row whose pid is alive\n*339|    // (e.g. recycled to this test process) but whose address has no listener\n 340|    // must NOT misdeliver or hang — connect refuses and the message spools.\n 341|    #[test]\n 342|    fn recycled_pid_address_does_not_misdeliver() {\n### emit.rs\n 10|//! Two edges, same grammar, one framing nuance:\n*11|//! - [`render_event_lines`] — the **listener stream** (`spt ready` /\n 12|//!   `spt api listen`, the `« spt event »` Monitor loops): composes **and**\n 13|//!   chunks oversized lines into `<EVENT-PART …>` groups so the Monitor\n 14|//!   notification cap holds.\n...\n 26|///\n*27|/// 1. a body that is already a fully-formed typed `<EVENT …>…</EVENT>` envelope\n 28|///    (echo_commune, notify, user-msg, …) passes through **verbatim** — never\n 29|///    re-wrapped, and the structural `from` is ignored in favor of the body's\n 30|///    own `from` (the echo-commune-double-envelope fix);\n...\n 41|\n*42|/// Render a delivered `(from, body)` into the listener-stream stdout line(s):\n 43|/// compose the `<EVENT>` (typed bodies pass through verbatim), then chunk an\n 44|/// oversized line into `<EVENT-PART …>` lines past [`EVENT_LINE_THRESHOLD`] so\n 45|/// the harness notification cap holds.\n...\n 137|    // [unit->REQ-MSG-ENVELOPE] the hook-drain edge composes a WHOLE plain-msg\n*138|    // <EVENT> — never an EVENT-PART, even for an oversized body the listener\n 139|    // would chunk. Self-delimiting by construction.\n 140|    #[test]\n 141|    fn whole_render_never_chunks() {\n...\n 150|\n*151|    // [unit->REQ-MSG-4] an oversized listener line chunks into EVENT-PART lines\n*152|    // that reassemble byte-exact (listener stream unchanged — still chunks).\n 153|    #[test]\n*154|    fn oversized_listener_line_chunks_into_event_parts() {\n 155|        let body = \"x\".repeat(1500);\n 156|        let lines = render_event_lines(\"doyle\", &body);\n 157|        assert!(lines.len() > 1, \"1500-char body must chunk\");\n### lib.rs\n 10|//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the\n*11|//! mis-elevated legacy reply-to framing). The delivery routing, listener\n*12|//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against\n 13|//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).\n 14|//!\n 15|//! M1 is **loopback-only**: every address is `127.0.0.1:<ephemeral>`. Cross-node\n*16|//! delivery is M4; the consolidated daemon that owns all listeners is M3. M1\n 17|//! ships the messaging *mechanism* on the proven M0 substrate.\n 18|// [impl->REQ-ARCH-1]\n 19|\n*20|// Modules land one per M1 task (T1 wire+listener, T2 deliver, T3 ready, T4 ring).\n 21|pub mod deliver;\n 22|pub mod emit;\n*23|pub mod listener;\n*24|pub mod ready;\n 25|pub mod ring;\n 26|pub mod wire;\n 27|\n*28|/// Test-only home isolation. The listener/ready/ring surfaces resolve the\n 29|/// owlery through `spt_store::perch::owlery_dir()`, which honors `$SPT_HOME`.\n 30|/// Without an override the default home is `%LOCALAPPDATA%\\spt-core` — distinct\n 31|/// from the modern spt runtime's `spt/` home, but still a real shared dir that\n### listener.rs\n*1|//! `PollListener` — a loopback TCP accept listener with registry lifecycle.\n 2|//!\n*3|//! Clean-room re-expression of the sister `common/listener.rs` against the\n 4|//! spt-core `spt-store` API (perch resolver + registry). The socket plumbing\n*5|//! and the cross-platform `poll_ready` syscall wrapper are mechanical and match\n 6|//! the sister byte-for-byte in behavior; the registry/info.json lifecycle is\n 7|//! re-pointed at `spt_store`.\n 8|//!\n...\n 11|//! (or [`PollListener::close`]) the registry entry is removed so no stale\n*12|//! address survives a crashed listener — the recycled-pid guard\n 13|//! ([`spt_store::registry::resolve_address`]) is the second line of defense.\n 14|//!\n*15|//! **Interim model (M1):** one standalone listener per ready agent. M3's\n*16|//! `spt-daemon` consolidates all listeners into one supervisor; the surface is\n 17|//! kept thin so that consolidation is a move, not a rewrite.\n 18|// [impl->REQ-MSG-1]\n 19|// [impl->REQ-MSG-3]\n...\n 24|use std::io::ErrorKind;\n*25|use std::net::{SocketAddr, TcpListener};\n 26|use std::path::PathBuf;\n 27|use std::time::Duration;\n 28|\n...\n 30|\n*31|/// A loopback TCP listener that owns its registry registration.\n 32|pub struct PollListener {\n*33|    listener: TcpListener,\n 34|    id: String,\n 35|    owlery: PathBuf,\n 36|    perch_path: PathBuf,\n...\n 41|\n*42|// poll_ready: wait for the listener socket to have a pending connection via the\n 43|// OS poll syscall. Returns true if readable, false on timeout/error.\n 44|\n 45|#[cfg(unix)]\n*46|fn poll_ready(listener: &TcpListener, timeout_ms: i32) -> bool {\n 47|    use std::os::unix::io::AsRawFd;\n 48|    let mut pfd = libc::pollfd {\n*49|        fd: listener.as_raw_fd(),\n 50|        events: libc::POLLIN,\n 51|        revents: 0,\n 52|    };\n...\n 66|#[cfg(windows)]\n*67|fn poll_ready(listener: &TcpListener, timeout_ms: i32) -> bool {\n 68|    use std::os::windows::io::AsRawSocket;\n 69|    use windows_sys::Win32::Networking::WinSock::{WSAPoll, POLLRDNORM, WSAPOLLFD};\n 70|    let mut pfd = WSAPOLLFD {\n*71|        fd: listener.as_raw_socket() as usize,\n 72|        events: POLLRDNORM,\n 73|        revents: 0,\n 74|    };\n...\n 96|\n*97|        let listener = TcpListener::bind(\"127.0.0.1:0\")\n*98|            .map_err(|e| format!(\"Failed to bind TCP listener: {}\", e))?;\n*99|        // Non-blocking listeners poll explicitly; blocking listeners keep the\n*100|        // OS default and gate accept behind poll_ready with a timeout.\n 101|        if timeout_ms.is_none() {\n*102|            listener\n 103|                .set_nonblocking(true)\n 104|                .map_err(|e| format!(\"Failed to set non-blocking: {}\", e))?;\n 105|        }\n*106|        let addr = listener\n 107|            .local_addr()\n 108|            .map_err(|e| format!(\"Failed to get local address: {}\", e))?;\n### ready.rs\n 6|//! chunking, alarms, handoff, Psyche) is **not** M1 — it lands in M2/M3. M1's\n*7|//! ready agent is the minimal participant: a perch + a TCP listener + a spool\n 8|//! drain.\n 9|//!\n 10|//! Startup ([`ReadyAgent::start`]) is the REQ-MSG-3 contract:\n*11|//! 1. Create the perch dir; write `info.json` (`ready_agent` endpoint) + the\n*12|//!    `ready` marker.\n*13|//! 2. Bind a loopback listener and register its address (via [`PollListener`]).\n 14|//! 3. Drain the **non-deferred** spool backlog so messages that arrived while\n 15|//!    the agent was offline are delivered on startup. Deferred rows are left\n 16|//!    for the hook channel (REQ-HAZARD-DEFERRED-SURVIVE-DRAIN, covered M0).\n...\n 18|//! Teardown ([`ReadyAgent::soft_cleanup`]) honors REQ-HAZARD-SOFT-CLEANUP (6.2):\n*19|//! it removes **only** the `ready` marker and unregisters the address. The\n 20|//! spool and `info.json` are preserved so a restart resumes its backlog.\n 21|// [impl->REQ-MSG-3]\n 22|\n...\n 29|\n*30|use crate::listener::PollListener;\n 31|\n 32|/// Default poll window for [`ReadyAgent::recv`]; bounds each blocking accept so\n 33|/// the caller stays responsive (and can re-check spool / shut down between calls).\n...\n 35|\n*36|/// A running ready agent: an owned perch + a registered loopback listener.\n 37|pub struct ReadyAgent {\n 38|    id: String,\n*39|    listener: PollListener,\n 40|    perch_path: PathBuf,\n 41|    closed: bool,\n 42|}\n...\n 44|impl ReadyAgent {\n*45|    /// Register the perch (info.json + ready marker + listener address) and\n 46|    /// drain the offline backlog. Returns the agent and any backlog messages\n 47|    /// (oldest first) that were spooled while it was down.\n 48|    // [impl->REQ-MSG-3]\n...\n 78|            &sid,\n*79|            \"ready_agent\",\n 80|        );\n 81|        let subnets = spt_store::subnet::SubnetStore::load();\n 82|        let mut vis = spt_store::visibility::VisibilityStore::load();\n 83|        // Nested-resolution seam (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): a NESTED id\n*84|        // derives its home from the PARENT perch, so a child-side ready verb needs no\n 85|        // `--subnet` the child can't know. Explicit still wins; a Self id falls through\n 86|        // to assign_home's auto/refuse policy unchanged. The impure parent read lives\n 87|        // here at the caller boundary, keeping stamp_creation_fields pure.\n...\n 104|\n*105|        // ready marker — the liveness signal senders check.\n*106|        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\n*107|            .map_err(|e| format!(\"Failed to write ready marker: {}\", e))?;\n 108|\n*109|        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind\n 110|        // writes info.json directly and — unlike the live `establish_perch` path\n*111|        // (startup.rs:250) — never ledgered, so an offline ready perch carried\n 112|        // ZERO session rows and the picker's offline Resume-from-history (which\n 113|        // gates on ledger rows) never offered it. Append a Boot row here, mirroring\n 114|        // the live path: the boot row dedups against a same-session re-bind, an\n...\n 124|                    trigger: spt_store::sessions::SessionTrigger::Boot,\n*125|                    // Mirror info.cwd if the ready bind tracked one (REQ-SESSION-RESUME-TEMPLATE).\n 126|                    cwd: rec.cwd.clone(),\n 127|                    // `append` stamps the persisted monotonic ordinal (REQ-DIGEST-CURSOR).\n 128|                    ordinal: None,\n*129|                    // Record the adapter this ready session runs under so a later\n 130|                    // Resume restores the same harness (D-2, REQ-SESSION-ADAPTER-RECORDED).\n 131|                    // [impl->REQ-SESSION-ADAPTER-RECORDED]\n 132|                    adapter: rec.adapter.clone(),\n...\n 143|\n*144|        // Bind + register the listener address. Blocking with a short poll so\n 145|        // recv() stays responsive.\n*146|        let listener = PollListener::bind_blocking(id, RECV_POLL)?;\n 147|\n 148|        // Drain the offline backlog (non-deferred only; deferred rows survive\n 149|        // for the hook channel). Carried structurally as (from, body) — the\n### ring.rs\n 23|use crate::deliver;\n*24|use crate::listener::PollListener;\n 25|\n*26|/// How often the reply wait polls its listener / spool.\n 27|const REPLY_POLL: Duration = Duration::from_millis(50);\n 28|\n 29|/// Outcome of a [`ring`].\n...\n 38|    Queued,\n*39|    /// Delivered to the ringer's *existing* live listener; the reply will\n 40|    /// surface there, so this call does not block-wait (avoids racing it).\n 41|    Delivered,\n 42|    /// No perch for the target at all.\n...\n 47|\n*48|/// True if `from` already has its own live listener (so we must not create an\n*49|/// ephemeral perch or block-wait — the reply will land on that listener). Routes\n 50|/// through the single liveness resolver (2.5): daemon-authoritative for hosted\n 51|/// perches, per-pid for external/interim ones — never an inline pid check.\n 52|// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n 53|fn from_has_live_perch(from: &str) -> bool {\n*54|    if !perch::resolve_ready_file(from, ParentHint::Infer).exists() {\n 55|        return false;\n 56|    }\n 57|    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n...\n 72|\n*73|    // If the ringer already has a live listener, deliver and let the reply land\n*74|    // there — blocking here would race that listener for the inbox.\n 75|    if from_has_live_perch(from) {\n 76|        deliver::deliver(target, from, trimmed, owlery);\n 77|        return RingOutcome::Delivered;\n...\n 80|    // --- Ephemeral reply perch. From here, EVERY return path cleans up. ---\n*81|    let reply_listener = match setup_ephemeral(from) {\n 82|        Ok(l) => l,\n 83|        Err(_) => {\n 84|            cleanup_ephemeral(from); // partial setup may have created the dir\n...\n 92|    if path == deliver::DeliveryPath::Spool {\n*93|        drop(reply_listener);\n 94|        cleanup_ephemeral(from);\n 95|        return RingOutcome::Queued;\n 96|    }\n 97|\n*98|    // Block for the reply: poll the listener and the spool until the deadline.\n 99|    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 100|    let deadline = Instant::now() + timeout;\n 101|    let outcome = loop {\n*102|        if let Some((from, body)) = reply_listener.check_message_blocking() {\n 103|            break RingOutcome::Replied { from, body };\n 104|        }\n 105|        if let Ok(Some(msg)) = spool::drain_one_at(&perch_path) {\n...\n 116|\n*117|    drop(reply_listener); // unregisters the address before we delete the perch\n 118|    cleanup_ephemeral(from);\n 119|    outcome\n 120|}\n 121|\n*122|/// Create an ephemeral reply perch for `from` (dir + info.json + ready marker)\n*123|/// and bind its listener. The perch looks online so the target's reply routes\n 124|/// back over TCP.\n 125|fn setup_ephemeral(from: &str) -> Result<PollListener, String> {\n 126|    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 127|    fs::create_dir_all(&perch_path).map_err(|e| format!(\"create ephemeral perch: {}\", e))?;\n*128|    let rec = InfoJson::new(from, \"once\", std::process::id(), \"ring\", \"ready_agent\");\n 129|    info::write_info(&perch_path, &rec).map_err(|e| format!(\"write info.json: {}\", e))?;\n*130|    fs::write(perch::resolve_ready_file(from, ParentHint::Infer), \"\")\n*131|        .map_err(|e| format!(\"write ready marker: {}\", e))?;\n 132|    PollListener::bind_blocking(from, REPLY_POLL)\n 133|}\n 134|\n*135|/// Remove an ephemeral perch entirely: ready marker, info.json, spool db, then\n 136|/// the perch dir. Best-effort + idempotent (every leftover is `let _ =`).\n 137|fn cleanup_ephemeral(from: &str) {\n 138|    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n*139|    let _ = fs::remove_file(perch::resolve_ready_file(from, ParentHint::Infer));\n 140|    let _ = fs::remove_file(perch::resolve_info_file(from, ParentHint::Infer));\n 141|    let _ = fs::remove_file(perch::resolve_spool_db(from, ParentHint::Infer));\n 142|    // Recursive: the wholly-owned ephemeral perch dir also carries the `.info.lock`\n### wire.rs\n 95|    use super::*;\n*96|    use std::net::{TcpListener, TcpStream};\n 97|\n 98|    // [unit->REQ-MSG-ENVELOPE] encode/decode carries (from, body) structurally —\n 99|    // no legacy reply-to string, body byte-ranges preserved (incl interior newlines).\n...\n 133|\n*134|    // [unit->REQ-MSG-1] a framed message round-trips client -> listener as (from, body).\n 135|    #[test]\n 136|    fn frame_round_trips_over_loopback() {\n*137|        let listener = TcpListener::bind(\"127.0.0.1:0\").unwrap();\n*138|        let addr = listener.local_addr().unwrap();\n 139|\n 140|        let handle = std::thread::spawn(move || {\n*141|            let (mut server, _) = listener.accept().unwrap();\n 142|            read_message(&mut server).unwrap()\n 143|        });\n 144|\n...\n 155|    fn oversized_length_prefix_rejected() {\n*156|        let listener = TcpListener::bind(\"127.0.0.1:0\").unwrap();\n*157|        let addr = listener.local_addr().unwrap();\n 158|\n 159|        let handle = std::thread::spawn(move || {\n*160|            let (mut server, _) = listener.accept().unwrap();\n 161|            read_message(&mut server)\n 162|        });\n 163|\n\n## spt-store/src/\n### registry.rs\n 9|//! A registered pid can die without unregistering (crash, kill). Resolving its\n*10|//! stale address would deliver to a dead/wrong listener. So [`clean_stale_entries`]\n 11|//! drops dead-pid rows, and [`resolve_address`] cleans **before** lookup. The\n 12|//! clean step is best-effort: a cleanup failure never hard-fails resolution\n 13|//! (the spool is the safe fallback on a TCP miss).\n...\n 55|// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\n*56|pub fn register_address(\n 57|    id: &str,\n 58|    addr: &std::net::SocketAddr,\n 59|    owlery: &Path,\n 60|) -> rusqlite::Result<()> {\n*61|    register_address_with_pid(id, addr, std::process::id(), owlery)\n 62|}\n 63|\n 64|/// Register (or replace) an agent's address under an explicit owning `pid`\n...\n 66|// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\n*67|pub fn register_address_with_pid(\n 68|    id: &str,\n 69|    addr: &std::net::SocketAddr,\n 70|    pid: u32,\n...\n 102|/// Remove an agent's entry.\n*103|pub fn unregister_address(id: &str, owlery: &Path) -> rusqlite::Result<()> {\n 104|    let conn = open_registry(owlery)?;\n 105|    conn.execute(\"DELETE FROM agents WHERE id = ?1\", params![id])?;\n 106|    Ok(())\n...\n 172|            handles.push(std::thread::spawn(move || {\n*173|                register_address_with_pid(\n 174|                    &format!(\"agent-{i}\"),\n 175|                    &addr(9000 + i),\n 176|                    std::process::id(),\n...\n 198|        let d = tempfile::tempdir().unwrap();\n*199|        register_address(\"doyle\", &addr(9000), d.path()).unwrap();\n 200|        assert_eq!(\n 201|            lookup_address(\"doyle\", d.path()),\n 202|            Some(\"127.0.0.1:9000\".to_string())\n...\n 212|        let d = tempfile::tempdir().unwrap();\n*213|        register_address_with_pid(\"live\", &addr(9001), std::process::id(), d.path()).unwrap();\n*214|        register_address_with_pid(\"dead\", &addr(9002), dead_pid(), d.path()).unwrap();\n 215|\n 216|        clean_stale_entries(d.path()).unwrap();\n 217|\n...\n 251|        set_status(&hosted, STATUS_OFFLINE).unwrap();\n*252|        register_address_with_pid(\"hosted\", &addr(9100), live, owlery).unwrap();\n 253|\n 254|        // External (no status), same live pid — must survive.\n*255|        register_address_with_pid(\"ext\", &addr(9101), live, owlery).unwrap();\n 256|\n 257|        clean_stale_entries(owlery).unwrap();\n 258|\n...\n 275|        let d = tempfile::tempdir().unwrap();\n*276|        register_address_with_pid(\"dead\", &addr(9003), dead_pid(), d.path()).unwrap();\n*277|        register_address_with_pid(\"live\", &addr(9004), std::process::id(), d.path()).unwrap();\n 278|\n 279|        assert_eq!(\n 280|            resolve_address(\"dead\", d.path()),\n...\n 308|\n*309|        register_address_with_pid(\"first\", &addr(9200), std::process::id(), &owlery)\n 310|            .expect(\"registry op on a fresh home must not CANTOPEN\");\n 311|\n 312|        assert!(owlery.exists(), \"owlery dir created by the registry open\");\n...\n 322|        let d = tempfile::tempdir().unwrap();\n*323|        register_address(\"doyle\", &addr(1111), d.path()).unwrap();\n*324|        register_address(\"doyle\", &addr(2222), d.path()).unwrap();\n 325|        assert_eq!(\n 326|            lookup_address(\"doyle\", d.path()),\n 327|            Some(\"127.0.0.1:2222\".to_string())\n### info.rs\n 4|//! - [`PidValue`] (untagged numeric-or-`\"BUSY\"`) is copied verbatim — the\n*5|//!   `\"BUSY\"` sentinel is load-bearing (a busy poll-listener writes it; numeric\n 6|//!   readers must treat it as \"no usable pid\").\n 7|//! - `state` is the spt-core [`EndpointType`](spt_proto::endpoint::EndpointType)\n 8|//!   tag string rather than the sister's `PerchState` enum (whose Spine/Touch\n...\n 25|/// Pid field that is either a real numeric pid or the `\"BUSY\"` sentinel a\n*26|/// listener writes while it holds the perch. Untagged: serializes as a bare\n 27|/// integer or a bare string. Copied verbatim (sister `PidValue`).\n 28|#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n 29|#[serde(untagged)]\n...\n 164|    /// the sole writers; no standalone setter). Absent ⇒ created by an\n*165|    /// adapterless surface (`spt ready`) or a pre-D9-5 record.\n 166|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n 167|    pub adapter: Option<String>,\n 168|    /// The CURRENT psyche-host-failure state (v0.8.1,\n...\n 197|    /// establish seam (carry-forward on revive). `None` ⇒ a pre-v0.10.0 record, an\n*198|    /// adapterless `ready`, or a non-live bind — the picker's status derivation\n 199|    /// type-gates so only a LiveAgent ever renders the amber \"harness-only\" state.\n 200|    /// `default` so every older record parses (N-1-safe, additive).\n 201|    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n...\n 312|///\n*313|/// Readers ([`read_info`]) stay LOCK-FREE: the atomic tmp+rename already gives a\n 314|/// reader a complete old-or-new record (KH-1.2); the lock serializes WRITERS\n 315|/// only. Errors if the record is absent/unreadable — the daemon only mutates\n 316|/// perches it bound.\n...\n 334|    mutate(&mut rec);\n*335|    // Already holding the lock — use the UNLOCKED writer (re-locking would deadlock).\n 336|    write_info_unlocked(perch_path, &rec)\n 337|}\n 338|\n...\n 470|/// REQ-WAKE-RESUME-LEG). `Some(reason)` records the most recent host/resume\n*471|/// failure as a ready-to-render F-1 string (e.g. an unregistered recorded adapter\n 472|/// at the wake-resume seam); `None` clears it on a successful host/bind so a\n 473|/// recovered endpoint shows clean.\n 474|///\n...\n 493|/// why the lock is on a constant sentinel inode, not on `info.json` (which is renamed on\n*494|/// every write). The perch directory must already exist.\n 495|fn lock_perch_sentinel(perch_path: &Path) -> std::io::Result<std::fs::File> {\n 496|    use fs2::FileExt;\n 497|    let lock = std::fs::OpenOptions::new()\n...\n 516|/// Atomically write `info` to `<perch_path>/info.json` (tmp + rename, EBUSY\n*517|/// retry via the atomic layer). The perch directory must already exist.\n 518|///\n 519|/// Uses the **durable** atomic write (fsync-before-rename): the perch record is\n 520|/// authoritative and its loss to a hard-reset NUL zero-fill is exactly the\n...\n 527|/// counter-39 #2 bind-vs-stamp lost update). This is ms-scale on these infrequent record\n*528|/// writes — the same lock-across-fsync every `mutate_info` already does — NOT the\n 529|/// per-keystroke W1b shape. Signature unchanged: every caller serializes for free.\n 530|// [impl->REQ-HAZARD-INFO-JSON-TORN-READ]\n 531|// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\n...\n 544|/// `establish_perch` check-then-write was exactly that lost-update race at bind). The\n*545|/// perch directory must already exist (the sentinel lives in it).\n 546|// [impl->REQ-HAZARD-INFO-RMW-LOST-UPDATE]\n 547|pub fn establish_locked<E>(\n 548|    perch_path: &Path,\n...\n 578|/// Read just the numeric pid, tolerantly. Routed through `serde_json::Value` so\n*579|/// a schema-minimal `{\"pid\": N}` (as wrapper-owned listeners write) parses\n 580|/// without the full `InfoJson` shape. Returns `None` for a missing file, the\n 581|/// `\"BUSY\"` sentinel, or any non-numeric pid. Copied semantics from sister\n 582|/// `get_pid_from_info`.\n...\n 638|            session_id: \"sid\".to_string(),\n*639|            state: \"ready_agent\".to_string(),\n 640|            parent_pid: None,\n 641|            cwd: None,\n 642|            status: None,\n...\n 1142|    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] read_pid tolerates a schema-minimal\n*1143|    // {\"pid\": N} record (wrapper-owned listener shape).\n 1144|    #[test]\n 1145|    fn read_pid_tolerates_schema_minimal() {\n 1146|        let d = perch();"}],"details":{"scopePath":"crates/spt/src/api, crates/spt-msg/src, crates/spt-store/src/registry.rs, crates/spt-store/src/info.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":200,"fileCount":16,"files":["crates/spt/src/api/auth.rs","crates/spt/src/api/delivery.rs","crates/spt/src/api/live.rs","crates/spt/src/api/mod.rs","crates/spt/src/api/reporting.rs","crates/spt/src/api/startup.rs","crates/spt/src/api/worker.rs","crates/spt-msg/src/deliver.rs","crates/spt-msg/src/emit.rs","crates/spt-msg/src/lib.rs","crates/spt-msg/src/listener.rs","crates/spt-msg/src/ready.rs","crates/spt-msg/src/ring.rs","crates/spt-msg/src/wire.rs","crates/spt-store/src/registry.rs","crates/spt-store/src/info.rs"],"fileMatches":[{"path":"crates/spt/src/api/auth.rs","count":3},{"path":"crates/spt/src/api/delivery.rs","count":2},{"path":"crates/spt/src/api/live.rs","count":3},{"path":"crates/spt/src/api/mod.rs","count":5},{"path":"crates/spt/src/api/reporting.rs","count":20},{"path":"crates/spt/src/api/startup.rs","count":20},{"path":"crates/spt/src/api/worker.rs","count":15},{"path":"crates/spt-msg/src/deliver.rs","count":20},{"path":"crates/spt-msg/src/emit.rs","count":7},{"path":"crates/spt-msg/src/lib.rs","count":7},{"path":"crates/spt-msg/src/listener.rs","count":20},{"path":"crates/spt-msg/src/ready.rs","count":20},{"path":"crates/spt-msg/src/ring.rs","count":20},{"path":"crates/spt-msg/src/wire.rs","count":8},{"path":"crates/spt-store/src/registry.rs","count":16},{"path":"crates/spt-store/src/info.rs","count":14}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt/src/api/\n### auth.rs\n  32│\n *33│/// Persist a capability token into an (already-created) perch dir.\n  34│pub fn write_token(perch_path: &Path, token: &str) -> std::io::Result<()> {\n  35│    std::fs::write(perch_path.join(TOKEN_FILE), token)\n  36│}\n    │...\n  97│        // which is how a live session would re-pin — AUTH_REFUSES forever (a silent,\n *98│        // permanent strand: ready:false, stale .idle, drains no-op, WAN spool sleeps).\n  99│        // Accept the caller's sid and RE-PIN. Same trust model as establish_perch's\n 100│        // dead-owner rebind (an orphaned perch accepts a new LOCAL owner exactly when\n 101│        // owner_alive == false). ADDITIVE to token auth: fires ONLY on (no/failed\n    │...\n 266│            &path,\n*267│            &InfoJson::new(id, \"2026-06-01T00:00:00Z\", pid, session_id, \"ready_agent\"),\n 268│        )\n 269│        .unwrap();\n 270│        let token = mint_token();\n### delivery.rs\n 181│/// everywhere rule (ADR-0020) covers only the harness arriving-message surfaces\n*182│/// on AGENT perches (`api listen`/`spt ready`/`api poll`/`api worker-poll`), not\n 183│/// this internal shell-command transport. Each row's body rides RAW, one per\n 184│/// line. **`notify_shell_e2e` is the regression guard — do not \"fix\" this back\n 185│/// into an `<EVENT>` wrap.**\n    │...\n 253│    // The claim/offer/release/evaporate core is the ONE shared `drain_idle_spool`\n*254│    // (spt_daemon::inject) — the same leg the send-time already-idle path and the\n 255│    // pulse-tick re-offer belt (REQ-IDLE-PARKED-DELIVERY) call, so the idle-EDGE drain\n 256│    // and the parked-idle drain cannot drift.\n 257│    let owlery = perch::owlery_dir();\n### live.rs\n  7│//! brain now runs it as scheduled in-process loops paced by a durable config.\n *8│//! This module keeps the binary's call sites stable — `cmd_listen` /\n  9│//! `cmd_shutdown` — by forwarding to `BrainLifecycle`. The logic lives once, in\n 10│//! the daemon crate; the duplicate here is gone.\n 11│//\n   │...\n 49│/// host from the manifest and runs the echo-before-teardown ordering (T6). A\n*50│/// non-live manifest (no Psyche) is a plain soft-stop: just drop the `ready`\n 51│/// marker. Auth-gated by the dispatcher.\n 52│pub fn cmd_shutdown(id: &str, manifest: Option<&Manifest>, install_dir: Option<&Path>) -> i32 {\n 53│    let session_id = perch_session_id(id);\n   │...\n 69│            // Non-live endpoint: soft-stop only (remove the online marker).\n*70│            let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n 71│            eprintln!(\"SHUTDOWN:{id} (non-live soft-stop)\");\n 72│            0\n 73│        }\n### mod.rs\n   7│//! perch/spool/registry directly via spt-store/spt-msg, mirroring M1's\n  *8│//! standalone listener. M3's `spt-daemon` swaps the on-disk seed + standalone\n   9│//! `listen` for an in-memory daemon record — the surface here stays thin so\n  10│//! that is a move, not a rewrite.\n  11│//!\n    │...\n 373│            session_id,\n*374│        } => startup::cmd_listen(\n 375│            &id,\n 376│            parent_pid,\n 377│            once,\n    │...\n 423│            let adapter = ctx.adapter.as_deref().filter(|a| !a.is_empty());\n*424│            startup::cmd_bind(\n 425│                &id,\n 426│                bind_session,\n 427│                auth.session_id,\n    │...\n 506│        // Read-only JSON report; the bare form self-resolves like whoami, so it takes\n*507│        // no auth (no mutation, exposes only what `endpoint list` already shows).\n 508│        ApiCmd::EndpointInfo { id } => reporting::cmd_endpoint_info(id.as_deref()),\n 509│        ApiCmd::HistoryLog { id, auth } => gated(&id, &auth, reporting::cmd_history_log),\n 510│        ApiCmd::DigestEntry { id, auth } => gated(&id, &auth, reporting::cmd_digest_entry),\n    │...\n 600│/// advertisement — hostable_types, `[env]`, `[session.notif]` (#5) — reflects\n*601│/// the profile. Returns an exit code on failure (already reported to stderr).\n 602│fn load_manifest(path: &PathBuf, adapter: &str) -> Result<Manifest, i32> {\n 603│    let text = std::fs::read_to_string(path).map_err(|e| {\n 604│        eprintln!(\"MANIFEST_READ:{}: {e}\", path.display());\n### reporting.rs\n   93│    // that instant, so the adapter's soft `api session-end` AUTHENTICATES and removes\n  *94│    // the ready marker (cmd_session_end:206). The boundary then rotates the sid but\n  *95│    // nothing re-writes ready → the perch reads is_online=false → try_spt_hosted_inject\n   96│    // Nones out on the CLI gate before any broker RPC → every post-clear force-native\n   97│    // (INCLUDING the checkpoint FIRE) reports the generic UNDELIVERED, FOREVER (SEAM-2,\n   98│    // B6's second half — the live-wake blocker). A boundary PROVES a live successor\n     │...\n  103│    // [impl->REQ-HAZARD-BOUNDARY-READY-STRAND]\n *104│    let _ = std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\");\n  105│    let _ = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE);\n  106│    // Append the rotated session to the ledger (REQ-TERM-6) so the digest spans\n  107│    // this boundary — enumerating the last K sessions, not just the new one. An\n     │...\n  161│    // one **deferred** shell-context row — hook-channel background context\n *162│    // that never wakes a listener (the live event-stream drain skips\n  163│    // deferred rows by design). Empty discovery (no instances, nothing\n  164│    // instantiable) injects nothing; best-effort like the notifs — a context\n  165│    // problem must never fail the boundary itself.\n     │...\n  252│    if !erase {\n *253│        // Soft (6.2): remove the ready marker; preserve spool/info.\n *254│        let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n  255│        // F-2 (REQ-SOFT-END-PRESERVES-LIVE-LISTENER): the relay address row is\n *256│        // LISTENER-scoped truth, not session-scoped — the poll listener SURVIVES\n  257│        // a /clear (whose adapter hook fires this soft end for the DEPARTING\n *258│        // session), and only the listener process knows its socket addr, so\n  259│        // deleting a live owner's row severs delivery unrecoverably (post-clear\n  260│        // sends hit NO_PERCH while the C-2 boundary re-stamp reads healthy).\n  261│        // Unregister ONLY a row whose owner is dead/offline per the ONE liveness\n     │...\n  268│        if !row_alive {\n *269│            let _ = registry::unregister_address(id, &owlery);\n  270│        }\n  271│        eprintln!(\"SESSION_END:{id} (soft; spool + history preserved)\");\n  272│        return 0;\n     │...\n  274│    // Erase: first GC the parent's REAPABLE workers (drained soft-stops + any\n *275│    // TTL-expired stale ready rows) so a leaked/stale worker never blocks the erase;\n  276│    // THEN cascade-guard only a GENUINELY-live remainder (REQ-WORKER-REAP + 6.3). The\n *277│    // parent is alive right now (we are erasing it), so the reap keeps a fresh ready\n  278│    // worker (in-flight) — which then correctly trips the cascade guard below.\n  279│    // [impl->REQ-WORKER-REAP]\n  280│    let _ = spt_store::worker_reap::reap_workers(\n     │...\n  289│    if !live.is_empty() {\n *290│        let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n  291│        eprintln!(\n  292│            \"CASCADE_GUARD:{id} hosts {} live child perch(es) — soft-cleaned, not erased:\",\n  293│            live.len()\n     │...\n  300│    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n *301│    let _ = registry::unregister_address(id, &owlery);\n  302│    if let Err(e) = std::fs::remove_dir_all(&perch_path) {\n  303│        eprintln!(\"SESSION_END_FAIL:{id}: {e}\");\n  304│        return EXIT_REFUSED;\n     │...\n  477│    let frame = spt_daemon::shellchan::compose_sensory_frame(&shell_id, sensory_type, payload);\n *478│    // Live-or-drop: TCP to the owner's live listener is the only delivery.\n  479│    if spt_msg::deliver::is_online(&owner)\n *480│        && spt_msg::deliver::deliver_tcp(&owner, \"\", &frame, &owlery)\n  481│    {\n  482│        eprintln!(\"SENSORY_DELIVERED:{shell_id} -> {owner} type={sensory_type}\");\n  483│    } else {\n     │...\n  655│        Ok(None) => {\n *656│            eprintln!(\"OWNER_SHUTDOWN_NOOP:{owner} (already resting)\");\n  657│            0\n  658│        }\n  659│        Err(e) => {\n     │...\n  790│/// has not yet seen; on a hit, mark it seen (durably) and return the formatted\n *791│/// line. Already-seen hits are skipped so a repeated keyword never permanently\n  792│/// blocks a later, distinct hint.\n  793│// [impl->REQ-MANIFEST-4]\n  794│fn select_and_mark_hint(\n     │...\n  807│        if seen.contains(&key) {\n *808│            continue; // already fired this session\n  809│        }\n  810│        if let Some(kw) = hint.matched_keyword(message) {\n  811│            let line = format!(\n     │...\n 1011│        info::write_info(&path, &rec).unwrap();\n*1012│        std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 1013│    }\n 1014│\n 1015│    // [unit->REQ-API-ENDPOINT-INFO] #7: attached_node is derived from the HONEST\n     │...\n 1192│            // shell-context row (M5-D3d agent awareness): the live event-stream\n*1193│            // drain skips it — hook-channel background context, never a listener\n 1194│            // wake — while a hook drain delivers it; empty discovery injects\n 1195│            // nothing at all.\n 1196│    fn boundary_injects_deferred_shell_context() {\n### startup.rs\n  96│    EmptySession,\n *97│    /// A *live* perch already holds this id under a different session (5.1).\n  98│    Conflict {\n  99│        id: String,\n 100│        held: String,\n    │...\n 116│/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.\n*117│/// Pure of any listener I/O so it is unit-testable without spawning processes.\n 118│/// Returns the perch's capability token on success.\n 119│///\n 120│/// REQ-START-3, refined by the F-034 leg-b split (doyle gate ruling, PR #80):\n    │...\n 216│/// live sid can revive that perch). Accepted local-surface posture: the\n*217│/// `--parent-pid` override already permits a local squat, so this stays a\n 218│/// doc+test qualification, not a redesign. The gates themselves hold exactly\n 219│/// as on a seeded bind: the parent-pid anchor must be ALIVE (5.1 pid-recycle\n 220│/// defense), `establish_perch` refuses a live-owner conflict, and the\n    │...\n 261│\n*262│/// Write the perch record (skeleton→live) + ready marker + capability token.\n 263│/// Reviving an existing perch keeps its token; a live perch held under a\n 264│/// different session id is a conflict (pid-recycle / id collision guard).\n 265│///\n    │...\n 277│#[allow(clippy::too_many_arguments)]\n*278│fn establish_perch(\n 279│    id: &str,\n 280│    session_id: &str,\n 281│    parent_pid: u32,\n    │...\n 382│        // Previously NEVER set on bind — the refuted v0.12.1 \"fresh agent shows under\n*383│        // its project\" P1. spt-hosted: cmd_bind passes its own current_dir (the broker\n 384│        // spawned it in project_cwd); harness-hosted: bind_from_seed passes the seed's\n 385│        // captured cwd. A revive carries the prior cwd forward (a re-bind must not wipe\n 386│        // it), like home/adapter/controllable.\n    │...\n 429│    };\n*430│    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\n 431│        .map_err(|e| BindError::Io(e.to_string()))?;\n 432│\n 433│    // Session ledger (REQ-TERM-6): record this session as active so the digest\n    │...\n 486│/// one code path both platforms; OS-native immediacy — Win job object / Linux PDEATHSIG\n*487│/// — is an OPTIONAL follow-up). The listener exits within one of these windows.\n 488│const PARENT_WATCH_POLL: std::time::Duration = std::time::Duration::from_secs(2);\n 489│\n*490│/// Process exit code when the listener self-exits because its `--parent-pid` host died\n 491│/// (REQ-HAZARD-LISTEN-ORPHAN) — distinct from a clean `0` or a refusal, so a log/operator\n 492│/// can tell an orphan-exit from a normal end.\n 493│const EXIT_PARENT_GONE: i32 = 3;\n 494│\n*495│/// One watchdog-tick decision (REQ-HAZARD-LISTEN-ORPHAN): the listener must exit once its\n 496│/// `--parent-pid` anchor is no longer alive. Pure over [`proc::is_process_alive`] so the\n 497│/// exit trigger is unit-testable without a real host fork.\n 498│// [impl->REQ-HAZARD-LISTEN-ORPHAN]\n    │...\n 502│\n*503│/// The loud orphan-exit line — names this listener + the dead parent, so a false-ONLINE\n 504│/// perch becomes a diagnosable exit, never a silent hang. Pure for the unit.\n 505│// [impl->REQ-HAZARD-LISTEN-ORPHAN]\n 506│fn listen_orphan_exit_line(id: &str, parent_pid: u32) -> String {\n    │...\n 508│        \"LISTEN_ORPHAN_EXIT:{id}: parent pid {parent_pid} is gone — exiting the orphaned \\\n*509│         listener so the perch flips OFFLINE (its recorded pid dies) and dead-owner rebind \\\n 510│         unblocks [REQ-HAZARD-LISTEN-ORPHAN]\"\n 511│    )\n 512│}\n    │...\n 514│/// Spawn the listen-orphan watchdog (REQ-HAZARD-LISTEN-ORPHAN). `--parent-pid` is the\n*515│/// auth anchor, but with no liveness watch a host death orphans the listener forever: the\n 516│/// perch stays falsely ONLINE, EVENTs stream to a dead stdout, and dead-owner rebind is\n 517│/// BLOCKED (the recorded pid is the live orphan). This bounded poll exits the process LOUD\n 518│/// within one [`PARENT_WATCH_POLL`] window on parent death, so the perch's recorded pid\n    │...\n 533│\n*534│pub fn cmd_listen(\n 535│    id: &str,\n 536│    parent_pid_override: Option<u32>,\n 537│    once: bool,\n    │...\n 616│    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n*617│    let relay = match spt_daemon::relay::Relay::bind(id, &perch_path) {\n 618│        Ok(r) => r,\n 619│        Err(e) => {\n 620│            eprintln!(\"LISTEN_FAIL:{id}: {e}\");\n    │...\n 670│    // Listen-orphan watchdog (REQ-HAZARD-LISTEN-ORPHAN): watch the `--parent-pid` host\n*671│    // and exit LOUD if it dies, so this listener never orphans the perch falsely ONLINE\n 672│    // (EVENTs to a dead stdout, dead-owner rebind blocked). Persistent branch only.\n 673│    // [impl->REQ-HAZARD-LISTEN-ORPHAN]\n 674│    spawn_parent_watchdog(id, parent_pid);\n    │...\n 691│// [impl->REQ-SEAM-POSTSPAWN]\n*692│pub fn cmd_bind(\n 693│    id: &str,\n 694│    set_session_id: Option<String>,\n 695│    proof_session: Option<String>,\n    │...\n 743│            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)\n*744│            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen\n 745│            // mark. A live_agent endpoint whose manifest declares\n 746│            // `[session.psyche_init]` is marked status=online at establish; the\n 747│            // brain's livehost reconcile then hosts the Psyche (the SAME single host\n 748│            // path — bind never spawns/hosts/calls livehost). Closes the\n*749│            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so\n 750│            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct\n 751│            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so\n 752│            // the establish-online signal == the reconcile host predicate (no\n    │...\n 791│\n*792│/// Emit one delivery to the listener stream: compose the structural\n 793│/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass\n 794│/// through; oversized lines chunk into EVENT-PART) and print + flush. One\n 795│/// delivery per emit.\n### worker.rs\n  10│//! A destructive parent teardown must never hard-delete a parent that still\n *11│//! hosts **live** children. \"Live\" here = the child still carries its `ready`\n  12│//! marker (the soft-cleanup signal; `worker-stop` removes it). When live\n  13│//! children exist, the caller soft-cleans and surfaces their paths instead of\n  14│//! recursively deleting in-flight work. [`live_child_paths`] is the guard\n    │...\n  94│    }\n *95│    // ready marker = the live-child signal the cascade-wipe guard checks.\n  96│    let _ = std::fs::write(\n *97│        perch::resolve_ready_file(&id, ParentHint::Explicit(parent)),\n  98│        \"\",\n  99│    );\n 100│    // STDOUT: the machine-readable result (bare minted id). STDERR: the human line.\n    │...\n 106│/// `api worker-stop <id>` — soft-clean a worker (REQ-HAZARD-SOFT-CLEANUP):\n*107│/// remove its `ready` marker so it no longer counts as a live child; the\n 108│/// `info.json` + spool are preserved. Idempotent.\n 109│// [impl->REQ-API-2]\n 110│pub fn cmd_worker_stop(id: &str) -> i32 {\n*111│    let removed = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer)).is_ok();\n 112│    if removed {\n 113│        eprintln!(\"WORKER_STOPPED:{id}\");\n 114│    } else {\n*115│        eprintln!(\"WORKER_STOPPED:{id} (no ready marker)\");\n 116│    }\n 117│    0\n 118│}\n    │...\n 145│/// The paths of `parent_id`'s **live** nested children (those still carrying a\n*146│/// `ready` marker). Empty ⇒ the parent is safe to hard-delete.\n 147│// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]\n 148│pub fn live_child_paths(parent_id: &str) -> Vec<std::path::PathBuf> {\n 149│    let nested = perch::resolve_perch_path(parent_id, ParentHint::Infer).join(\"nested\");\n    │...\n 158│        }\n*159│        // A child is live while its ready marker exists (info.json present =\n*160│        // it's a real perch; ready = not yet soft-cleaned).\n*161│        if info::read_info(&child).is_some() && child.join(\"ready\").exists() {\n 162│            live.push(child);\n 163│        }\n 164│    }\n    │...\n 199│        assert!(expected.join(\"info.json\").exists());\n*200│        assert!(expected.join(\"ready\").exists());\n 201│        // Inferred resolution agrees with the explicit nesting.\n 202│        assert_eq!(\n 203│            perch::resolve_perch_path(\"alice-w1\", ParentHint::Infer),\n    │...\n 250│            &ppath,\n*251│            &InfoJson::new(\"alice\", \"0\", std::process::id(), \"\", \"ready_agent\"),\n 252│        )\n 253│        .unwrap();\n 254│        assert_eq!(cmd_worker_start(\"alice\", None, None, Some(\"presented-sid\")), 0);\n    │...\n 275│\n*276│        // Soft-stop the worker → ready marker gone → no longer a live child.\n 277│        assert_eq!(cmd_worker_stop(\"alice-w1\"), 0);\n 278│        assert!(\n 279│            live_child_paths(\"alice\").is_empty(),\n    │...\n 281│        );\n*282│        // The record is preserved (soft cleanup), only the ready marker removed.\n 283│        let child = perch::resolve_perch_path(\"alice-w1\", ParentHint::Infer);\n 284│        assert!(child.join(\"info.json\").exists());\n*285│        assert!(!child.join(\"ready\").exists());\n 286│    }\n 287│\n 288│    #[test] // [unit->REQ-API-2] worker-poll drains the worker's own spool.\n\n## spt-msg/src/\n### deliver.rs\n  17│//! Two hazards ride on step 2/3:\n *18│//! - **Windows pid-recycle (5.1):** a dead listener's pid can be recycled to an\n  19│//!   unrelated live process, so a \"pid alive\" check alone can't prove the perch\n  20│//!   is real. The structural guard is that *connect must succeed* — a recycled\n  21│//!   process is not listening on the old ephemeral port, so the connect refuses\n    │...\n  46│pub enum DeliveryPath {\n *47│    /// Delivered live over TCP to the target's listener.\n  48│    Tcp,\n  49│    /// Target unreachable — written to the target's spool for later drain.\n  50│    Spool,\n    │...\n  71│\n *72│/// True if `target` is online: its `ready` marker exists and the single liveness\n  73│/// resolver ([`spt_store::liveness::is_perch_alive`]) reports it live. That\n  74│/// resolver is daemon-authoritative for daemon-hosted perches (their `status`\n  75│/// field) and per-pid for external/interim perches (a `BUSY`/unparseable pid with\n *76│/// a present `ready` marker reads alive — the listener is up between polls). The\n  77│/// per-pid assumption is **not** duplicated here (2.5) — it lives only in the\n  78│/// resolver, so the daemon swap stays localized.\n  79│// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n  80│pub fn is_online(target: &str) -> bool {\n *81│    if !perch::resolve_ready_file(target, ParentHint::Infer).exists() {\n  82│        return false;\n  83│    }\n  84│    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n    │...\n  95│// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]\n *96│pub fn deliver_tcp(target: &str, from: &str, body: &str, owlery: &Path) -> bool {\n  97│    if let Some(addr_str) = registry::resolve_address(target, owlery) {\n  98│        if let Ok(addr) = addr_str.parse::<SocketAddr>() {\n  99│            match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {\n    │...\n 109│                    // is \"alive\" (possibly recycled to an unrelated process), the\n*110│                    // refused connect already prevented misdelivery — the caller\n 111│                    // falls back.\n 112│                    if let Some(pid) = registry::lookup_pid(target, owlery) {\n 113│                        if !proc::is_process_alive(pid) {\n*114│                            let _ = registry::unregister_address(target, owlery);\n 115│                        }\n 116│                    }\n 117│                }\n    │...\n 131│    //    double-delivery).\n*132│    if deliver_tcp(target, from, body, owlery) {\n 133│        return DeliveryPath::Tcp;\n 134│    }\n 135│\n    │...\n 165│\n*166│/// Deferred send (the hook channel): spool-only, **no TCP, no listener wake**.\n 167│/// The row is tagged `deferred=1` so the live event-stream drain\n 168│/// (`drain_non_deferred_at`) skips it — it surfaces only when a hook consumer\n 169│/// drains everything (`drain_all_at` / `peek_all_at`) on its natural cadence.\n    │...\n 216│    // active_only never wakes — spool straight to the hook channel.\n*217│    if window != spool::WINDOW_ACTIVE_ONLY && deliver_tcp(target, from, trimmed, owlery) {\n 218│        return SendOutcome::Sent;\n 219│    }\n 220│    let _ = spool::spool_message_windowed_at(\n    │...\n 233│    use super::*;\n*234│    use crate::listener::PollListener;\n 235│    use spt_store::info;\n 236│    use std::net::SocketAddr;\n 237│\n    │...\n 253│        let id = unique_id(\"test-online\");\n*254│        let listener = PollListener::bind(&id).unwrap();\n 255│\n 256│        let path = deliver(&id, \"sender\", \"live msg\", &owlery);\n 257│        assert_eq!(path, DeliveryPath::Tcp);\n    │...\n 261│        for _ in 0..200 {\n*262│            if let Some(m) = listener.check_message() {\n 263│                got = Some(m);\n 264│                break;\n 265│            }\n    │...\n 293│        info::write_info(&perch_path, &rec).unwrap();\n*294│        let listener = PollListener::bind(&id).unwrap();\n 295│\n 296│        let outcome =\n 297│            send_windowed(&id, \"peer\", \"background ctx\", &owlery, spool::WINDOW_ACTIVE_ONLY, false);\n    │...\n 306│            assert!(\n*307│                listener.check_message().is_none(),\n 308│                \"active_only must never reach a relay's live poll channel (never-wake)\"\n 309│            );\n 310│            std::thread::sleep(Duration::from_millis(5));\n    │...\n 319│\n*320│    // [unit->REQ-MSG-1] offline target (perch exists, no listener) spools.\n 321│    #[test]\n 322│    fn offline_existing_perch_spools() {\n 323│        crate::test_home::isolate();\n    │...\n 326│        // Create the perch dir + a minimal info.json so perch_exists is true,\n*327│        // but bind no listener (offline).\n 328│        let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);\n 329│        std::fs::create_dir_all(&perch_path).unwrap();\n*330│        let rec = info::InfoJson::new(&id, \"now\", std::process::id(), \"sess\", \"ready_agent\");\n 331│        info::write_info(&perch_path, &rec).unwrap();\n 332│\n 333│        let path = deliver(&id, \"sender\", \"queued msg\", &owlery);\n    │...\n 338│    // [unit->REQ-HAZARD-WINDOWS-PID-RECYCLE] a registry row whose pid is alive\n*339│    // (e.g. recycled to this test process) but whose address has no listener\n 340│    // must NOT misdeliver or hang — connect refuses and the message spools.\n 341│    #[test]\n 342│    fn recycled_pid_address_does_not_misdeliver() {\n### emit.rs\n  10│//! Two edges, same grammar, one framing nuance:\n *11│//! - [`render_event_lines`] — the **listener stream** (`spt ready` /\n  12│//!   `spt api listen`, the `« spt event »` Monitor loops): composes **and**\n  13│//!   chunks oversized lines into `<EVENT-PART …>` groups so the Monitor\n  14│//!   notification cap holds.\n    │...\n  26│///\n *27│/// 1. a body that is already a fully-formed typed `<EVENT …>…</EVENT>` envelope\n  28│///    (echo_commune, notify, user-msg, …) passes through **verbatim** — never\n  29│///    re-wrapped, and the structural `from` is ignored in favor of the body's\n  30│///    own `from` (the echo-commune-double-envelope fix);\n    │...\n  41│\n *42│/// Render a delivered `(from, body)` into the listener-stream stdout line(s):\n  43│/// compose the `<EVENT>` (typed bodies pass through verbatim), then chunk an\n  44│/// oversized line into `<EVENT-PART …>` lines past [`EVENT_LINE_THRESHOLD`] so\n  45│/// the harness notification cap holds.\n    │...\n 137│    // [unit->REQ-MSG-ENVELOPE] the hook-drain edge composes a WHOLE plain-msg\n*138│    // <EVENT> — never an EVENT-PART, even for an oversized body the listener\n 139│    // would chunk. Self-delimiting by construction.\n 140│    #[test]\n 141│    fn whole_render_never_chunks() {\n    │...\n 150│\n*151│    // [unit->REQ-MSG-4] an oversized listener line chunks into EVENT-PART lines\n*152│    // that reassemble byte-exact (listener stream unchanged — still chunks).\n 153│    #[test]\n*154│    fn oversized_listener_line_chunks_into_event_parts() {\n 155│        let body = \"x\".repeat(1500);\n 156│        let lines = render_event_lines(\"doyle\", &body);\n 157│        assert!(lines.len() > 1, \"1500-char body must chunk\");\n### lib.rs\n 10│//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the\n*11│//! mis-elevated legacy reply-to framing). The delivery routing, listener\n*12│//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against\n 13│//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).\n 14│//!\n 15│//! M1 is **loopback-only**: every address is `127.0.0.1:<ephemeral>`. Cross-node\n*16│//! delivery is M4; the consolidated daemon that owns all listeners is M3. M1\n 17│//! ships the messaging *mechanism* on the proven M0 substrate.\n 18│// [impl->REQ-ARCH-1]\n 19│\n*20│// Modules land one per M1 task (T1 wire+listener, T2 deliver, T3 ready, T4 ring).\n 21│pub mod deliver;\n 22│pub mod emit;\n*23│pub mod listener;\n*24│pub mod ready;\n 25│pub mod ring;\n 26│pub mod wire;\n 27│\n*28│/// Test-only home isolation. The listener/ready/ring surfaces resolve the\n 29│/// owlery through `spt_store::perch::owlery_dir()`, which honors `$SPT_HOME`.\n 30│/// Without an override the default home is `%LOCALAPPDATA%\\spt-core` — distinct\n 31│/// from the modern spt runtime's `spt/` home, but still a real shared dir that\n### listener.rs\n  *1│//! `PollListener` — a loopback TCP accept listener with registry lifecycle.\n   2│//!\n  *3│//! Clean-room re-expression of the sister `common/listener.rs` against the\n   4│//! spt-core `spt-store` API (perch resolver + registry). The socket plumbing\n  *5│//! and the cross-platform `poll_ready` syscall wrapper are mechanical and match\n   6│//! the sister byte-for-byte in behavior; the registry/info.json lifecycle is\n   7│//! re-pointed at `spt_store`.\n   8│//!\n    │...\n  11│//! (or [`PollListener::close`]) the registry entry is removed so no stale\n *12│//! address survives a crashed listener — the recycled-pid guard\n  13│//! ([`spt_store::registry::resolve_address`]) is the second line of defense.\n  14│//!\n *15│//! **Interim model (M1):** one standalone listener per ready agent. M3's\n *16│//! `spt-daemon` consolidates all listeners into one supervisor; the surface is\n  17│//! kept thin so that consolidation is a move, not a rewrite.\n  18│// [impl->REQ-MSG-1]\n  19│// [impl->REQ-MSG-3]\n    │...\n  24│use std::io::ErrorKind;\n *25│use std::net::{SocketAddr, TcpListener};\n  26│use std::path::PathBuf;\n  27│use std::time::Duration;\n  28│\n    │...\n  30│\n *31│/// A loopback TCP listener that owns its registry registration.\n  32│pub struct PollListener {\n *33│    listener: TcpListener,\n  34│    id: String,\n  35│    owlery: PathBuf,\n  36│    perch_path: PathBuf,\n    │...\n  41│\n *42│// poll_ready: wait for the listener socket to have a pending connection via the\n  43│// OS poll syscall. Returns true if readable, false on timeout/error.\n  44│\n  45│#[cfg(unix)]\n *46│fn poll_ready(listener: &TcpListener, timeout_ms: i32) -> bool {\n  47│    use std::os::unix::io::AsRawFd;\n  48│    let mut pfd = libc::pollfd {\n *49│        fd: listener.as_raw_fd(),\n  50│        events: libc::POLLIN,\n  51│        revents: 0,\n  52│    };\n    │...\n  66│#[cfg(windows)]\n *67│fn poll_ready(listener: &TcpListener, timeout_ms: i32) -> bool {\n  68│    use std::os::windows::io::AsRawSocket;\n  69│    use windows_sys::Win32::Networking::WinSock::{WSAPoll, POLLRDNORM, WSAPOLLFD};\n  70│    let mut pfd = WSAPOLLFD {\n *71│        fd: listener.as_raw_socket() as usize,\n  72│        events: POLLRDNORM,\n  73│        revents: 0,\n  74│    };\n    │...\n  96│\n *97│        let listener = TcpListener::bind(\"127.0.0.1:0\")\n *98│            .map_err(|e| format!(\"Failed to bind TCP listener: {}\", e))?;\n *99│        // Non-blocking listeners poll explicitly; blocking listeners keep the\n*100│        // OS default and gate accept behind poll_ready with a timeout.\n 101│        if timeout_ms.is_none() {\n*102│            listener\n 103│                .set_nonblocking(true)\n 104│                .map_err(|e| format!(\"Failed to set non-blocking: {}\", e))?;\n 105│        }\n*106│        let addr = listener\n 107│            .local_addr()\n 108│            .map_err(|e| format!(\"Failed to get local address: {}\", e))?;\n### ready.rs\n   6│//! chunking, alarms, handoff, Psyche) is **not** M1 — it lands in M2/M3. M1's\n  *7│//! ready agent is the minimal participant: a perch + a TCP listener + a spool\n   8│//! drain.\n   9│//!\n  10│//! Startup ([`ReadyAgent::start`]) is the REQ-MSG-3 contract:\n *11│//! 1. Create the perch dir; write `info.json` (`ready_agent` endpoint) + the\n *12│//!    `ready` marker.\n *13│//! 2. Bind a loopback listener and register its address (via [`PollListener`]).\n  14│//! 3. Drain the **non-deferred** spool backlog so messages that arrived while\n  15│//!    the agent was offline are delivered on startup. Deferred rows are left\n  16│//!    for the hook channel (REQ-HAZARD-DEFERRED-SURVIVE-DRAIN, covered M0).\n    │...\n  18│//! Teardown ([`ReadyAgent::soft_cleanup`]) honors REQ-HAZARD-SOFT-CLEANUP (6.2):\n *19│//! it removes **only** the `ready` marker and unregisters the address. The\n  20│//! spool and `info.json` are preserved so a restart resumes its backlog.\n  21│// [impl->REQ-MSG-3]\n  22│\n    │...\n  29│\n *30│use crate::listener::PollListener;\n  31│\n  32│/// Default poll window for [`ReadyAgent::recv`]; bounds each blocking accept so\n  33│/// the caller stays responsive (and can re-check spool / shut down between calls).\n    │...\n  35│\n *36│/// A running ready agent: an owned perch + a registered loopback listener.\n  37│pub struct ReadyAgent {\n  38│    id: String,\n *39│    listener: PollListener,\n  40│    perch_path: PathBuf,\n  41│    closed: bool,\n  42│}\n    │...\n  44│impl ReadyAgent {\n *45│    /// Register the perch (info.json + ready marker + listener address) and\n  46│    /// drain the offline backlog. Returns the agent and any backlog messages\n  47│    /// (oldest first) that were spooled while it was down.\n  48│    // [impl->REQ-MSG-3]\n    │...\n  78│            &sid,\n *79│            \"ready_agent\",\n  80│        );\n  81│        let subnets = spt_store::subnet::SubnetStore::load();\n  82│        let mut vis = spt_store::visibility::VisibilityStore::load();\n  83│        // Nested-resolution seam (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): a NESTED id\n *84│        // derives its home from the PARENT perch, so a child-side ready verb needs no\n  85│        // `--subnet` the child can't know. Explicit still wins; a Self id falls through\n  86│        // to assign_home's auto/refuse policy unchanged. The impure parent read lives\n  87│        // here at the caller boundary, keeping stamp_creation_fields pure.\n    │...\n 104│\n*105│        // ready marker — the liveness signal senders check.\n*106│        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\n*107│            .map_err(|e| format!(\"Failed to write ready marker: {}\", e))?;\n 108│\n*109│        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind\n 110│        // writes info.json directly and — unlike the live `establish_perch` path\n*111│        // (startup.rs:250) — never ledgered, so an offline ready perch carried\n 112│        // ZERO session rows and the picker's offline Resume-from-history (which\n 113│        // gates on ledger rows) never offered it. Append a Boot row here, mirroring\n 114│        // the live path: the boot row dedups against a same-session re-bind, an\n    │...\n 124│                    trigger: spt_store::sessions::SessionTrigger::Boot,\n*125│                    // Mirror info.cwd if the ready bind tracked one (REQ-SESSION-RESUME-TEMPLATE).\n 126│                    cwd: rec.cwd.clone(),\n 127│                    // `append` stamps the persisted monotonic ordinal (REQ-DIGEST-CURSOR).\n 128│                    ordinal: None,\n*129│                    // Record the adapter this ready session runs under so a later\n 130│                    // Resume restores the same harness (D-2, REQ-SESSION-ADAPTER-RECORDED).\n 131│                    // [impl->REQ-SESSION-ADAPTER-RECORDED]\n 132│                    adapter: rec.adapter.clone(),\n    │...\n 143│\n*144│        // Bind + register the listener address. Blocking with a short poll so\n 145│        // recv() stays responsive.\n*146│        let listener = PollListener::bind_blocking(id, RECV_POLL)?;\n 147│\n 148│        // Drain the offline backlog (non-deferred only; deferred rows survive\n 149│        // for the hook channel). Carried structurally as (from, body) — the\n### ring.rs\n  23│use crate::deliver;\n *24│use crate::listener::PollListener;\n  25│\n *26│/// How often the reply wait polls its listener / spool.\n  27│const REPLY_POLL: Duration = Duration::from_millis(50);\n  28│\n  29│/// Outcome of a [`ring`].\n    │...\n  38│    Queued,\n *39│    /// Delivered to the ringer's *existing* live listener; the reply will\n  40│    /// surface there, so this call does not block-wait (avoids racing it).\n  41│    Delivered,\n  42│    /// No perch for the target at all.\n    │...\n  47│\n *48│/// True if `from` already has its own live listener (so we must not create an\n *49│/// ephemeral perch or block-wait — the reply will land on that listener). Routes\n  50│/// through the single liveness resolver (2.5): daemon-authoritative for hosted\n  51│/// perches, per-pid for external/interim ones — never an inline pid check.\n  52│// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n  53│fn from_has_live_perch(from: &str) -> bool {\n *54│    if !perch::resolve_ready_file(from, ParentHint::Infer).exists() {\n  55│        return false;\n  56│    }\n  57│    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n    │...\n  72│\n *73│    // If the ringer already has a live listener, deliver and let the reply land\n *74│    // there — blocking here would race that listener for the inbox.\n  75│    if from_has_live_perch(from) {\n  76│        deliver::deliver(target, from, trimmed, owlery);\n  77│        return RingOutcome::Delivered;\n    │...\n  80│    // --- Ephemeral reply perch. From here, EVERY return path cleans up. ---\n *81│    let reply_listener = match setup_ephemeral(from) {\n  82│        Ok(l) => l,\n  83│        Err(_) => {\n  84│            cleanup_ephemeral(from); // partial setup may have created the dir\n    │...\n  92│    if path == deliver::DeliveryPath::Spool {\n *93│        drop(reply_listener);\n  94│        cleanup_ephemeral(from);\n  95│        return RingOutcome::Queued;\n  96│    }\n  97│\n *98│    // Block for the reply: poll the listener and the spool until the deadline.\n  99│    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 100│    let deadline = Instant::now() + timeout;\n 101│    let outcome = loop {\n*102│        if let Some((from, body)) = reply_listener.check_message_blocking() {\n 103│            break RingOutcome::Replied { from, body };\n 104│        }\n 105│        if let Ok(Some(msg)) = spool::drain_one_at(&perch_path) {\n    │...\n 116│\n*117│    drop(reply_listener); // unregisters the address before we delete the perch\n 118│    cleanup_ephemeral(from);\n 119│    outcome\n 120│}\n 121│\n*122│/// Create an ephemeral reply perch for `from` (dir + info.json + ready marker)\n*123│/// and bind its listener. The perch looks online so the target's reply routes\n 124│/// back over TCP.\n 125│fn setup_ephemeral(from: &str) -> Result<PollListener, String> {\n 126│    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 127│    fs::create_dir_all(&perch_path).map_err(|e| format!(\"create ephemeral perch: {}\", e))?;\n*128│    let rec = InfoJson::new(from, \"once\", std::process::id(), \"ring\", \"ready_agent\");\n 129│    info::write_info(&perch_path, &rec).map_err(|e| format!(\"write info.json: {}\", e))?;\n*130│    fs::write(perch::resolve_ready_file(from, ParentHint::Infer), \"\")\n*131│        .map_err(|e| format!(\"write ready marker: {}\", e))?;\n 132│    PollListener::bind_blocking(from, REPLY_POLL)\n 133│}\n 134│\n*135│/// Remove an ephemeral perch entirely: ready marker, info.json, spool db, then\n 136│/// the perch dir. Best-effort + idempotent (every leftover is `let _ =`).\n 137│fn cleanup_ephemeral(from: &str) {\n 138│    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n*139│    let _ = fs::remove_file(perch::resolve_ready_file(from, ParentHint::Infer));\n 140│    let _ = fs::remove_file(perch::resolve_info_file(from, ParentHint::Infer));\n 141│    let _ = fs::remove_file(perch::resolve_spool_db(from, ParentHint::Infer));\n 142│    // Recursive: the wholly-owned ephemeral perch dir also carries the `.info.lock`\n### wire.rs\n  95│    use super::*;\n *96│    use std::net::{TcpListener, TcpStream};\n  97│\n  98│    // [unit->REQ-MSG-ENVELOPE] encode/decode carries (from, body) structurally —\n  99│    // no legacy reply-to string, body byte-ranges preserved (incl interior newlines).\n    │...\n 133│\n*134│    // [unit->REQ-MSG-1] a framed message round-trips client -> listener as (from, body).\n 135│    #[test]\n 136│    fn frame_round_trips_over_loopback() {\n*137│        let listener = TcpListener::bind(\"127.0.0.1:0\").unwrap();\n*138│        let addr = listener.local_addr().unwrap();\n 139│\n 140│        let handle = std::thread::spawn(move || {\n*141│            let (mut server, _) = listener.accept().unwrap();\n 142│            read_message(&mut server).unwrap()\n 143│        });\n 144│\n    │...\n 155│    fn oversized_length_prefix_rejected() {\n*156│        let listener = TcpListener::bind(\"127.0.0.1:0\").unwrap();\n*157│        let addr = listener.local_addr().unwrap();\n 158│\n 159│        let handle = std::thread::spawn(move || {\n*160│            let (mut server, _) = listener.accept().unwrap();\n 161│            read_message(&mut server)\n 162│        });\n 163│\n\n## spt-store/src/\n### registry.rs\n   9│//! A registered pid can die without unregistering (crash, kill). Resolving its\n *10│//! stale address would deliver to a dead/wrong listener. So [`clean_stale_entries`]\n  11│//! drops dead-pid rows, and [`resolve_address`] cleans **before** lookup. The\n  12│//! clean step is best-effort: a cleanup failure never hard-fails resolution\n  13│//! (the spool is the safe fallback on a TCP miss).\n    │...\n  55│// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\n *56│pub fn register_address(\n  57│    id: &str,\n  58│    addr: &std::net::SocketAddr,\n  59│    owlery: &Path,\n  60│) -> rusqlite::Result<()> {\n *61│    register_address_with_pid(id, addr, std::process::id(), owlery)\n  62│}\n  63│\n  64│/// Register (or replace) an agent's address under an explicit owning `pid`\n    │...\n  66│// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\n *67│pub fn register_address_with_pid(\n  68│    id: &str,\n  69│    addr: &std::net::SocketAddr,\n  70│    pid: u32,\n    │...\n 102│/// Remove an agent's entry.\n*103│pub fn unregister_address(id: &str, owlery: &Path) -> rusqlite::Result<()> {\n 104│    let conn = open_registry(owlery)?;\n 105│    conn.execute(\"DELETE FROM agents WHERE id = ?1\", params![id])?;\n 106│    Ok(())\n    │...\n 172│            handles.push(std::thread::spawn(move || {\n*173│                register_address_with_pid(\n 174│                    &format!(\"agent-{i}\"),\n 175│                    &addr(9000 + i),\n 176│                    std::process::id(),\n    │...\n 198│        let d = tempfile::tempdir().unwrap();\n*199│        register_address(\"doyle\", &addr(9000), d.path()).unwrap();\n 200│        assert_eq!(\n 201│            lookup_address(\"doyle\", d.path()),\n 202│            Some(\"127.0.0.1:9000\".to_string())\n    │...\n 212│        let d = tempfile::tempdir().unwrap();\n*213│        register_address_with_pid(\"live\", &addr(9001), std::process::id(), d.path()).unwrap();\n*214│        register_address_with_pid(\"dead\", &addr(9002), dead_pid(), d.path()).unwrap();\n 215│\n 216│        clean_stale_entries(d.path()).unwrap();\n 217│\n    │...\n 251│        set_status(&hosted, STATUS_OFFLINE).unwrap();\n*252│        register_address_with_pid(\"hosted\", &addr(9100), live, owlery).unwrap();\n 253│\n 254│        // External (no status), same live pid — must survive.\n*255│        register_address_with_pid(\"ext\", &addr(9101), live, owlery).unwrap();\n 256│\n 257│        clean_stale_entries(owlery).unwrap();\n 258│\n    │...\n 275│        let d = tempfile::tempdir().unwrap();\n*276│        register_address_with_pid(\"dead\", &addr(9003), dead_pid(), d.path()).unwrap();\n*277│        register_address_with_pid(\"live\", &addr(9004), std::process::id(), d.path()).unwrap();\n 278│\n 279│        assert_eq!(\n 280│            resolve_address(\"dead\", d.path()),\n    │...\n 308│\n*309│        register_address_with_pid(\"first\", &addr(9200), std::process::id(), &owlery)\n 310│            .expect(\"registry op on a fresh home must not CANTOPEN\");\n 311│\n 312│        assert!(owlery.exists(), \"owlery dir created by the registry open\");\n    │...\n 322│        let d = tempfile::tempdir().unwrap();\n*323│        register_address(\"doyle\", &addr(1111), d.path()).unwrap();\n*324│        register_address(\"doyle\", &addr(2222), d.path()).unwrap();\n 325│        assert_eq!(\n 326│            lookup_address(\"doyle\", d.path()),\n 327│            Some(\"127.0.0.1:2222\".to_string())\n### info.rs\n    4│//! - [`PidValue`] (untagged numeric-or-`\"BUSY\"`) is copied verbatim — the\n   *5│//!   `\"BUSY\"` sentinel is load-bearing (a busy poll-listener writes it; numeric\n    6│//!   readers must treat it as \"no usable pid\").\n    7│//! - `state` is the spt-core [`EndpointType`](spt_proto::endpoint::EndpointType)\n    8│//!   tag string rather than the sister's `PerchState` enum (whose Spine/Touch\n     │...\n   25│/// Pid field that is either a real numeric pid or the `\"BUSY\"` sentinel a\n  *26│/// listener writes while it holds the perch. Untagged: serializes as a bare\n   27│/// integer or a bare string. Copied verbatim (sister `PidValue`).\n   28│#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n   29│#[serde(untagged)]\n     │...\n  164│    /// the sole writers; no standalone setter). Absent ⇒ created by an\n *165│    /// adapterless surface (`spt ready`) or a pre-D9-5 record.\n  166│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n  167│    pub adapter: Option<String>,\n  168│    /// The CURRENT psyche-host-failure state (v0.8.1,\n     │...\n  197│    /// establish seam (carry-forward on revive). `None` ⇒ a pre-v0.10.0 record, an\n *198│    /// adapterless `ready`, or a non-live bind — the picker's status derivation\n  199│    /// type-gates so only a LiveAgent ever renders the amber \"harness-only\" state.\n  200│    /// `default` so every older record parses (N-1-safe, additive).\n  201│    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n     │...\n  312│///\n *313│/// Readers ([`read_info`]) stay LOCK-FREE: the atomic tmp+rename already gives a\n  314│/// reader a complete old-or-new record (KH-1.2); the lock serializes WRITERS\n  315│/// only. Errors if the record is absent/unreadable — the daemon only mutates\n  316│/// perches it bound.\n     │...\n  334│    mutate(&mut rec);\n *335│    // Already holding the lock — use the UNLOCKED writer (re-locking would deadlock).\n  336│    write_info_unlocked(perch_path, &rec)\n  337│}\n  338│\n     │...\n  470│/// REQ-WAKE-RESUME-LEG). `Some(reason)` records the most recent host/resume\n *471│/// failure as a ready-to-render F-1 string (e.g. an unregistered recorded adapter\n  472│/// at the wake-resume seam); `None` clears it on a successful host/bind so a\n  473│/// recovered endpoint shows clean.\n  474│///\n     │...\n  493│/// why the lock is on a constant sentinel inode, not on `info.json` (which is renamed on\n *494│/// every write). The perch directory must already exist.\n  495│fn lock_perch_sentinel(perch_path: &Path) -> std::io::Result<std::fs::File> {\n  496│    use fs2::FileExt;\n  497│    let lock = std::fs::OpenOptions::new()\n     │...\n  516│/// Atomically write `info` to `<perch_path>/info.json` (tmp + rename, EBUSY\n *517│/// retry via the atomic layer). The perch directory must already exist.\n  518│///\n  519│/// Uses the **durable** atomic write (fsync-before-rename): the perch record is\n  520│/// authoritative and its loss to a hard-reset NUL zero-fill is exactly the\n     │...\n  527│/// counter-39 #2 bind-vs-stamp lost update). This is ms-scale on these infrequent record\n *528│/// writes — the same lock-across-fsync every `mutate_info` already does — NOT the\n  529│/// per-keystroke W1b shape. Signature unchanged: every caller serializes for free.\n  530│// [impl->REQ-HAZARD-INFO-JSON-TORN-READ]\n  531│// [impl->REQ-HAZARD-PERCH-RECORD-POWER-LOSS]\n     │...\n  544│/// `establish_perch` check-then-write was exactly that lost-update race at bind). The\n *545│/// perch directory must already exist (the sentinel lives in it).\n  546│// [impl->REQ-HAZARD-INFO-RMW-LOST-UPDATE]\n  547│pub fn establish_locked<E>(\n  548│    perch_path: &Path,\n     │...\n  578│/// Read just the numeric pid, tolerantly. Routed through `serde_json::Value` so\n *579│/// a schema-minimal `{\"pid\": N}` (as wrapper-owned listeners write) parses\n  580│/// without the full `InfoJson` shape. Returns `None` for a missing file, the\n  581│/// `\"BUSY\"` sentinel, or any non-numeric pid. Copied semantics from sister\n  582│/// `get_pid_from_info`.\n     │...\n  638│            session_id: \"sid\".to_string(),\n *639│            state: \"ready_agent\".to_string(),\n  640│            parent_pid: None,\n  641│            cwd: None,\n  642│            status: None,\n     │...\n 1142│    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] read_pid tolerates a schema-minimal\n*1143│    // {\"pid\": N} record (wrapper-owned listener shape).\n 1144│    #[test]\n 1145│    fn read_pid_tolerates_schema_minimal() {\n 1146│        let d = perch();"},"isError":false,"timestamp":1784197397378}}
{"type":"message","id":"d682b013","parentId":"028a72ea","timestamp":"2026-07-16T10:23:17.381Z","message":{"role":"toolResult","toolCallId":"call_P1OR6n8pmTUXbOV8tFFU5Wal|fc_017bff223290b7ba016a58b1155054819ab50703ec7d54ded0","toolName":"grep","content":[{"type":"text","text":"# CONTEXT.md\n 16|**spt.exe / spt** (canonical binary):\n*17|The 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 18|\n 19|**library workspace**:\n 20|The 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 29|**spt-daemon** (per-machine supervisor):\n*30|The 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 siste...\n 31|\n 32|Internally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\n 33|- **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...\n 38|**in-session relay**:\n*39|A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle de...\n 40|\n 41|### Deliverable shape\n 42|\n...\n 45|- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.\n*46|- **`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 47|\n 48|Both 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 49|\n...\n 149|- **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*150|- **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-car...\n 151|Window 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 152|\n 153|**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never ...\n...\n 180|- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.\n*181|- **`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 182|- **`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 183|- **`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 184|- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).\n...\n 207|   - 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*208|   - 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 209|\n 210|**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:\n 211|1. the seed's `parent_pid` → that process's **executable basename** (case-insensitive, `.exe`-stripped);\n...\n 232|**ReadyAgent**:\n*233|Minimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's \"ready agent\".\n 234|\n 235|**LiveAgent**:\n 236|A 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 425|brings up a **LiveAgent** (the daemon reconcile hosts its Psyche); a manifest *without*\n*426|it brings up a **ReadyAgent** (a poll listener, no Psyche — see *ReadyAgent* and the\n 427|harness-hosted ready bind at the *seed + bind-time resolution* note above). No\n 428|`--adapter`/picker branch distinguishes them: the daemon live-host reconcile hosts only a\n 429|perch whose **state is `live_agent`** (a `ready_agent`-state perch is skipped at the\n...\n 663|<!-- [doc->REQ-ENDPOINT-UNBOUND-ATTACH] -->\n*664|**Unbound endpoint**:\n*665|The lifecycle point between *spawn* and *bind*: an spt-hosted endpoint whose broker **session + PTY are live** but whose harness has **not yet bound** its perch (the *post-spawn seam* hasn't fired — e.g. the harness is waiting on a startup prompt). On-disk status `unbound` (spawn → `unbound`; bind → `online`; session death → `offline`). An Unbound endpoint is **attachable** (a live PTY — `spt rc` and the `endpoint run` attach reach it, so an operator can see and drive the harness, including cle...\n 666|\n 667|<!-- [doc->REQ-INST-14] -->\n 668|**resource advertisement (subnet resource registry)**:\n...\n 740|**joiner / seed-holder (ceremony roles)**:\n*741|The **joiner** is the new, not-yet-trusted node running the ceremony's initiator side (`spt subnet join`, typed code). A **seed-holder** is any already-trusted member node holding the subnet seed; every member is one, and any online member's daemon answers the join rendezvous as responder — always-on, no arming step (the user interacts only with the new node; the subnet-global rate limiter is the standing-listener guard).\n 742|\n 743|**TOTP-seeded SPAKE2 pairing**:\n 744|The day-one pairing model. A durable **TOTP seed** is the subnet secret: generated on the first node, shown as a QR → stored in the user's authenticator app *and* held by every already-trusted node. To pair a new node, the rotating 6-digit TOTP code is used as the **password for a SPAKE2 (PAKE) handshake** — *not* as a bearer token verified by a seed-holder. A trusted node (online, no human needed — it holds the seed) computes the current code as its PAKE password; the user reads the code off their...\n...\n 766|\n*767|**Node-bound code fetch — and every subnet-membership mutation — is gated behind OS privilege elevation** (Windows UAC / Linux root-or-equivalent): retrieving a subnet's code *from the node*, minting a new subnet (`subnet create` — a seed reveal), and joining one (`subnet join` — enrolling the machine into a trust fabric) all require either hardware/elevated access OR an **elevated endpoint** (an agent whose process is elevated can surface it). The join gate exists because membership is a trust-b...\n 768|\n 769|**self-elevating re-launch (cross-platform)**:\n 770|<!-- [doc->REQ-ELEVATE-1] -->\n...\n 786|**subnet attachment (attached / detached)**:\n*787|Whether this node's daemon is **actively serving** a held subnet membership right now — pairing responder reachable, rendezvous meet listener rotating, registry gossip pumping. **Attached** = serving; **detached** = the membership record (seed) is held on disk but deliberately not served (the daemon neither advertises into nor connects to that subnet). Detachment is a *chosen* per-subnet state — `spt subnet detach/attach <NAME> [--save]` (shipped M8-D2; `--save` persists the startup default in daemon...\n 788|\n 789|**endpoint description** (canonical CLI term for the resource-advertisement blurb):\n 790|The per-endpoint free-text \"yellow-pages\" line (see §resource advertisement). Surface term is **description**; \"blurb\" survives only as the internal field name.\n\n# docs/\n## KNOWN-HAZARDS.md\n 4|\n*5|**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it...\n 6|\n 7|---\n 8|\n...\n 45|### 2.1 Parent PID over ephemeral poll PID\n*46|- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\n 47|- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\n 48|- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\n 49|- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n...\n 72|- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`inf...\n*73|- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let ...\n 74|- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n 75|\n 76|---\n...\n 86|### 3.2 Stale signoff sentinel must not kill a fresh start\n*87|- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\n*88|- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\n 89|- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\n 90|- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\n 91|\n...\n 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 119|- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n...\n 281|- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interlea...\n*282|- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field w...\n 283|- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\n 284|\n 285|---\n...\n 658|- **Failure (paid-for, perri field finding 2026-07-08 — F-032, data-loss):** `ingest_drops` unconditionally deleted the drop after `route_slices`, but the project tier is GATED on a non-empty `project_id` — when the endpoint's anchor cwd was unresolved/owlery-internal at ingest time, the `<project-context>` slice was parsed but never committed, yet the source drop was still deleted → the project-context content permanently lost. perri's two-sliced echo-commune INGESTED (file deleted) yet never surf...\n*659|- **Invariant:** delete-after-commit, per slice. Distinguish a write **SUPPRESSED by precedence** (incoming older than durable → already superseded → safe to delete) from a slice **un-committable now** (empty `project_id`, or any write error → the `?` retry path). An un-committable non-empty project slice is preserved as a **project-only pending slice under the COMMUNE suffix, whatever the source drop's kind** — a signoff-suffix pending would be reaped by `sweep_stale_signoff` on the next listene...\n 660|- **spt-core mapping:** `spt-live/src/ingest.rs` `ingest_drops` (deferred-project preserve + `Ingested.preserved`); `route_slices` project gate unchanged (`REQ-STORE-CONTEXT-BRANCH-FILL`); kin `REQ-HAZARD-DROP-FILE-SINGLE-WRITER` (core stays the single writer — the preserve rewrite is core's own write).\n 661|- **Source:** MSG-IDENTITY W3 (doyle triage, code-grounded ingest.rs:156 gate vs :200 delete; repro fixture F-032-commune-2026-07-08T222721Z.md).\n 662|\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 10|\n*11|1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.\n 12|\n 13|2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive \"drain + restart the daemon\" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).\n 14|\n...\n 16|\n*17|**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` — no separate listener or wrapper processes. The only residue is a thin, stateless **in-session relay** for harness-hosted sessions (topology 1), where spt cannot reach into a process tree it doesn't own; the relay just pipes the daemon's events into the session's stdout and is freely killable.\n 18|\n 19|**Split the logical daemon into two implementation layers** to satisfy the no-terminate invariant:\n 20|- **broker** (stable kernel) — holds *only* un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets. Minimal, versioned local IPC. Almost never updates.\n...\n 32|\n*33|- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.\n 34|- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of \"literally one process,\" preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.\n 35|- Peer-propagated updates make release signing mandatory, not optional.\n 36|- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n### 0005-totp-seeded-spake2-node-pairing.md\n 68|  node pubkey, and a relay routes one endpoint per key), and a per-subnet\n*69|  rendezvous **meet** listener rotates with the TOTP step. There is no arming\n 70|  step: this completes the one-sided UX this ADR intended (\"a seed-holder\n*71|  node must be online and reachable\"). The standing-listener exposure is\n 72|  guarded by the #11 subnet-global rate limiter, a one-ceremony-at-a-time\n 73|  shed at the accept door, and a ceremony timeout aligned with the limiter's\n*74|  stale-slot reclaim — an attacker reaching the listener still gets one\n 75|  online guess per admitted ceremony, serialized and backed off.\n 76|- **Membership mutations are elevation-gated** (the REQ-PAIR-6 machinery,\n 77|  exit 3, gate-before-everything): `subnet create` reveals a fresh subnet's\n### 0018-broker-brain-process-isolation-restoration.md\n 36|- **Sequencing:** the restoration is the next milestone, before `spt-claude-code` scoping (operator-accepted 2026-06-09). Rationale: it is the *last* release that needs a manual fleet daemon bounce — paid 3× for v0.3.2 — so every adapter-era release rolls seamlessly; and the adapter is better built on the final topology than atop a daemon under later surgery while it hosts the user's daily driver. The split changes daemon internals, not the M8-frozen CLI/api surface.\n*37|- **Out of scope (unchanged):** the broker-touching update classes (broker-compatible / broker-breaking) remain as ADR-0004 left them; whole-daemon FD-passing stays the deferred \"future polish\" (ADR-0004:38); a durable in-daemon alarm scheduler is a separate gap (alarms are legacy-listener-only today).\n### 0020-event-envelope-sole-arriving-format-reply-to-removed.md\n 19|  adapters (\"their consumer is the adapter's hook composer, not the event\n*20|  stream\"), while the **listener stream** (`api listen` / `spt ready`) already\n 21|  renders the canonical `<EVENT>` envelope via `render_event_lines`.\n 22|\n 23|The result: the spool stores `(from_id, body)` columns → `format_row` serializes\n*24|them into the `__REPLY_TO__:` string → for the listener, `render_event_lines`\n 25|parses that string **back** into `(from, body)` to compose `<EVENT>`. A pointless\n 26|round-trip through a relic, and `api poll` hands the relic straight to adapters.\n 27|\n...\n 57|     single-line `<EVENT …>…</EVENT>` per message** (body `<br>`-escaped). The\n*58|     **listener stream** additionally chunks an oversized line into\n 59|     `<EVENT-PART seq=\"N/M\">` groups so the **`« spt event »` Monitor's ~500-char\n 60|     notification cap** holds (`EVENT_LINE_THRESHOLD`) — a harness-notification\n 61|     concern. The **hook drains (`api poll`/`worker-poll`) emit whole `<EVENT>`s,\n...\n 130|    `emit.rs::parse_frame` removed (compose from structural `(from, body)`);\n*131|    `deliver.rs` / `listener.rs` / `ring.rs` / `ready.rs` rebind; `lib.rs`\n 132|    ADR-0001 framing corrected (the `<EVENT>` envelope is the stable format, not\n 133|    the TCP frame).\n 134|  - `spt` — `api poll` / `api worker-poll` (`delivery.rs::emit`) compose `<EVENT>`\n### 0022-spt-hosted-idle-delivery-translation-binary.md\n 21|\n*22|Today there is also a coverage gap: `api bind` registers no listener port, so a listener-less\n 23|spt-hosted perch *spools* inbound messages — only spooling + adapter-poll works.\n 24|\n 25|We previously locked (Q1 → \"A+\", CCS session 16531b26, 2026-06-17) a **unified** model: the daemon's\n...\n 49|  spt-core owning every PTY write is what lets injection coexist with a live `spt rc` controller.\n*50|- **Poll-listener preference:** spt-core prefers a perch's poll listener if one exists, so an\n*51|  spt-hosted endpoint can run a listener AND keep `spt rc`. A dev may keep the perch \"idle-ready\"\n*52|  and use either the listener or the translation binary for *all* delivery.\n 53|- **Scope:** idle delivery only. Busy/mid-turn delivery stays adapter hook-injection.\n 54|\n 55|The v0.11.0 raw inject is the **degenerate special case** — a translation binary that emits\n...\n 109|- **Binary hosts its own `spt api listen` + writes the PTY directly.** Rejected: spt-core already\n*110|  owns listener logic, and if the binary wrote the PTY directly spt-core could not coordinate\n 111|  atomicity with the controller. Piping the feed to stdin and reading keystrokes from stdout keeps\n 112|  the binary trivial and keeps every PTY write under spt-core's control.\n 113|- **Keep two distinct delivery paths (Q1 option B).** Rejected in favour of the unified poll-feed\n### 0026-multi-subnet-home-at-endpoint-run-creation.md\n 6|\n*7|accepted (2026-06-23; v0.14.0 endpoint-creation-flow, W6 int keystone green — `multi_subnet_bringup_e2e` proves the multi-subnet refuse + `--subnet` bind-inherit on a ≥2-subnet node, no double-seed). Refines ADR-0010 (immutable home subnet) at the spt-hosted creation seam. Sibling of ADR-0027 (Unbound state + attach-on-session). Delivered in the endpoint-creation-flow milestone (post-v0.13.2).\n 8|\n 9|## Context\n 10|\n### 0027-unbound-endpoint-state-and-attach-on-session.md\n*1|# Unbound endpoint state + attach-on-session-exists\n 2|\n 3|<!-- [doc->REQ-ENDPOINT-UNBOUND-ATTACH] -->\n 4|\n...\n 6|\n*7|accepted (2026-06-23; v0.14.0 endpoint-creation-flow, W6 int keystone green — `multi_subnet_bringup_e2e` proves a fresh hold-unbound endpoint is `spt rc`-attachable pre-bind, and `picker::data::tests::gather_renders_live_unbound` proves the real on-disk→render Unbound seam). Sibling of ADR-0026 (multi-subnet home at creation). Delivered in the endpoint-creation-flow milestone (post-v0.13.2).\n 8|\n 9|## Context\n 10|\n...\n 18|\n*19|- **Attach-on-session-exists.** `cmd_endpoint_run` (and `spt rc <id>`) attach as soon as a **live broker session** exists for the id — regardless of perch status — so the operator sees and drives the harness immediately, including clearing a bind-gating prompt. This also makes a **headless** bringup attachable before/without bind. The source of truth is the broker `sessions` map (the per-endpoint registry, ADR-0025 W3a). This *preserves* REQ-HAZARD-RC-ATTACH-ONLINE-RACE's \"don't attach before there's...\n 20|\n 21|- **New on-disk status `STATUS_UNBOUND`.** The daemon stamps `UNBOUND` at spawn; `bind` flips it to `ONLINE`; session death → `OFFLINE`. (Chosen over a purely-derived display so the state is explicit and queryable in the daemon-authoritative liveness model.)\n 22|  - **Lifecycle reuses the existing reconcile/exit-waiter** — a dead broker session → `OFFLINE` (the same path that marks dead-but-latched perches offline). No new teardown; `UNBOUND` auto-resolves (→`ONLINE` on bind, →`OFFLINE` on session death).\n...\n 35|- A new on-disk liveness status (`STATUS_UNBOUND`) — the liveness predicates and reconcile/teardown must treat it honestly (attachable, not message-addressable; auto-resolves).\n*36|- `spt rc <id>` works against an unbound endpoint (a live session with no bind) — useful for headless bringups and for clearing a bind-gating prompt.\n 37|- The attach gate moves earlier (session-exists), removing the prompt-before-bind deadlock while keeping the original race protection.\n 38|- `EpDisplay` grows (hollow + hollow-controlled); the picker/legend gain the Unbound state.\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"CONTEXT.md, docs/KNOWN-HAZARDS.md, docs/adr","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":43,"fileCount":9,"files":["CONTEXT.md","docs/KNOWN-HAZARDS.md","docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","docs/adr/0005-totp-seeded-spake2-node-pairing.md","docs/adr/0018-broker-brain-process-isolation-restoration.md","docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md","docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md","docs/adr/0026-multi-subnet-home-at-endpoint-run-creation.md","docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md"],"fileMatches":[{"path":"CONTEXT.md","count":14},{"path":"docs/KNOWN-HAZARDS.md","count":8},{"path":"docs/adr/0004-single-daemon-broker-brain-split-and-self-update.md","count":3},{"path":"docs/adr/0005-totp-seeded-spake2-node-pairing.md","count":3},{"path":"docs/adr/0018-broker-brain-process-isolation-restoration.md","count":1},{"path":"docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md","count":4},{"path":"docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md","count":5},{"path":"docs/adr/0026-multi-subnet-home-at-endpoint-run-creation.md","count":1},{"path":"docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md","count":4}],"truncated":true,"displayContent":"# CONTEXT.md\n  16│**spt.exe / spt** (canonical binary):\n *17│The 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  18│\n  19│**library workspace**:\n  20│The 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  29│**spt-daemon** (per-machine supervisor):\n *30│The 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 siste...\n  31│\n  32│Internally the logical daemon is split into two implementation layers for seamless self-update (see Self-update):\n  33│- **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    │...\n  38│**in-session relay**:\n *39│A thin, stateless `spt.exe` task that exists only in **harness-hosted** sessions (where the agent harness is the parent process and spt cannot reach into its process tree — today's Monitor model). It streams the daemon brain's events into the session's stdout. All *stateful* listener logic lives in the daemon; the relay is a dumb pipe, freely killable and respawnable. **spt-hosted** sessions need no separate harness-owned relay — the daemon owns the PTY and consumes the same poll feed itself. Idle de...\n  40│\n  41│### Deliverable shape\n  42│\n    │...\n  45│- **Library crates** — the deeper integration path. Used by future first-party services that link Rust directly.\n *46│- **`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  47│\n  48│Both 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  49│\n    │...\n 149│- **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*150│- **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-car...\n 151│Window 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 152│\n 153│**message metadata (`json`)** — a sender may attach an opaque JSON metadata block (`--json-payload`), carried as a single attr-escaped `json=\"…\"` envelope attribute **alongside** (never replacing) the body. spt-core never interprets it — pure verbatim passthrough across every rail (spool / TCP / WAN / EVENT-PART), parsed only by the receiving adapter (its hooks and/or translation binary). Collision-proof by construction: the structured data lives **inside** the single `json` value, so it can never ...\n    │...\n 180│- **`api bind`** — post-spawn boot bind (payload above). Skeleton→live.\n*181│- **`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 182│- **`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 183│- **`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 184│- **`api worker-start`** / **`api worker-stop`** — Worker (subagent) perch create/teardown under the parent (nested, registry-tracked).\n    │...\n 207│   - 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*208│   - 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 209│\n 210│**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:\n 211│1. the seed's `parent_pid` → that process's **executable basename** (case-insensitive, `.exe`-stripped);\n    │...\n 232│**ReadyAgent**:\n*233│Minimal SPT participant — a perch + a poll listener, no Psyche, no live-agent wrapper. Direct heir to the sister project's \"ready agent\".\n 234│\n 235│**LiveAgent**:\n 236│A 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 425│brings up a **LiveAgent** (the daemon reconcile hosts its Psyche); a manifest *without*\n*426│it brings up a **ReadyAgent** (a poll listener, no Psyche — see *ReadyAgent* and the\n 427│harness-hosted ready bind at the *seed + bind-time resolution* note above). No\n 428│`--adapter`/picker branch distinguishes them: the daemon live-host reconcile hosts only a\n 429│perch whose **state is `live_agent`** (a `ready_agent`-state perch is skipped at the\n    │...\n 663│<!-- [doc->REQ-ENDPOINT-UNBOUND-ATTACH] -->\n*664│**Unbound endpoint**:\n*665│The lifecycle point between *spawn* and *bind*: an spt-hosted endpoint whose broker **session + PTY are live** but whose harness has **not yet bound** its perch (the *post-spawn seam* hasn't fired — e.g. the harness is waiting on a startup prompt). On-disk status `unbound` (spawn → `unbound`; bind → `online`; session death → `offline`). An Unbound endpoint is **attachable** (a live PTY — `spt rc` and the `endpoint run` attach reach it, so an operator can see and drive the harness, including cle...\n 666│\n 667│<!-- [doc->REQ-INST-14] -->\n 668│**resource advertisement (subnet resource registry)**:\n    │...\n 740│**joiner / seed-holder (ceremony roles)**:\n*741│The **joiner** is the new, not-yet-trusted node running the ceremony's initiator side (`spt subnet join`, typed code). A **seed-holder** is any already-trusted member node holding the subnet seed; every member is one, and any online member's daemon answers the join rendezvous as responder — always-on, no arming step (the user interacts only with the new node; the subnet-global rate limiter is the standing-listener guard).\n 742│\n 743│**TOTP-seeded SPAKE2 pairing**:\n 744│The day-one pairing model. A durable **TOTP seed** is the subnet secret: generated on the first node, shown as a QR → stored in the user's authenticator app *and* held by every already-trusted node. To pair a new node, the rotating 6-digit TOTP code is used as the **password for a SPAKE2 (PAKE) handshake** — *not* as a bearer token verified by a seed-holder. A trusted node (online, no human needed — it holds the seed) computes the current code as its PAKE password; the user reads the code off their...\n    │...\n 766│\n*767│**Node-bound code fetch — and every subnet-membership mutation — is gated behind OS privilege elevation** (Windows UAC / Linux root-or-equivalent): retrieving a subnet's code *from the node*, minting a new subnet (`subnet create` — a seed reveal), and joining one (`subnet join` — enrolling the machine into a trust fabric) all require either hardware/elevated access OR an **elevated endpoint** (an agent whose process is elevated can surface it). The join gate exists because membership is a trust-b...\n 768│\n 769│**self-elevating re-launch (cross-platform)**:\n 770│<!-- [doc->REQ-ELEVATE-1] -->\n    │...\n 786│**subnet attachment (attached / detached)**:\n*787│Whether this node's daemon is **actively serving** a held subnet membership right now — pairing responder reachable, rendezvous meet listener rotating, registry gossip pumping. **Attached** = serving; **detached** = the membership record (seed) is held on disk but deliberately not served (the daemon neither advertises into nor connects to that subnet). Detachment is a *chosen* per-subnet state — `spt subnet detach/attach <NAME> [--save]` (shipped M8-D2; `--save` persists the startup default in daemon...\n 788│\n 789│**endpoint description** (canonical CLI term for the resource-advertisement blurb):\n 790│The per-endpoint free-text \"yellow-pages\" line (see §resource advertisement). Surface term is **description**; \"blurb\" survives only as the internal field name.\n\n# docs/\n## KNOWN-HAZARDS.md\n   4│\n  *5│**Architecture-translation note.** The sister project runs poll listeners and Psyche wrappers as *separate processes*. spt-core consolidates both into the one `spt-daemon` (brain), with a stable broker beneath it (ADR-0004). Many hazards below were inter-process races in the sister project; in spt-core some become intra-daemon concerns (potentially easier) while others move to the daemon↔broker IPC boundary or the network boundary (potentially new failure surface). Each entry notes the mapping where it...\n   6│\n   7│---\n   8│\n    │...\n  45│### 2.1 Parent PID over ephemeral poll PID\n *46│- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\n  47│- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\n  48│- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\n  49│- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n    │...\n  72│- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`inf...\n *73│- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let ...\n  74│- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n  75│\n  76│---\n    │...\n  86│### 3.2 Stale signoff sentinel must not kill a fresh start\n *87│- **Failure:** a leftover `.claude/<id>-signoff.md` from a prior session is read by a fresh listener as a live signoff → immediate teardown.\n *88│- **Invariant:** on every listener/daemon spawn, sweep stale signoff sentinels; signoff files are write-once per generation.\n  89│- **spt-core mapping:** same sweep on daemon (re)start per hosted instance.\n  90│- **Sister cite:** CHANGELOG v1.11.20; `src/owl/cleanup.rs:97`.\n  91│\n    │...\n 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 119│- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n    │...\n 281│- **Invariant:** every `info.json` writer serializes under the one per-perch `.info.lock`. A whole-record write (`write_info`) takes the lock exactly as the RMW (`mutate_info`) does — a unique tmp only makes concurrent writes *last-writer-wins*, which is safe ONLY if the writers are serialized (cross-ref 5.15). A multi-step read→check→write (bind's establish) must hold ONE lock acquisition across all three (a true compare-and-set) — check-then-write with the lock dropped in between still interlea...\n*282│- **spt-core mapping:** `spt_store::info` — `write_info` now acquires the sentinel then calls a private `write_info_unlocked` (and `mutate_info` — the public RMW primitive — calls the unlocked writer while holding its own lock, so no double-lock deadlock); `info::establish_locked(perch, build)` runs the bind's read→check→build→write as one locked CAS, called by `spt::api::startup::establish_perch`. The other read-modify-write callers were audited and the two that mutate a load-bearing field w...\n 283│- **Source:** RCA 2026-07-01 (doyle gate rig) — differential (baseline PASS / 77beeac+baseline-atomic PASS / fe385f5 2/3 FAIL) isolated the atomic rework as the delta; `reconcile_hosted_liveness` emitting ZERO `LIVENESS_RECONCILE_OFFLINE` lines for the dead victim over 20s pinned the silent-skip. The unique-tmp fix worked (bringup + serve fine) but exposed this deeper pairing.\n 284│\n 285│---\n    │...\n 658│- **Failure (paid-for, perri field finding 2026-07-08 — F-032, data-loss):** `ingest_drops` unconditionally deleted the drop after `route_slices`, but the project tier is GATED on a non-empty `project_id` — when the endpoint's anchor cwd was unresolved/owlery-internal at ingest time, the `<project-context>` slice was parsed but never committed, yet the source drop was still deleted → the project-context content permanently lost. perri's two-sliced echo-commune INGESTED (file deleted) yet never surf...\n*659│- **Invariant:** delete-after-commit, per slice. Distinguish a write **SUPPRESSED by precedence** (incoming older than durable → already superseded → safe to delete) from a slice **un-committable now** (empty `project_id`, or any write error → the `?` retry path). An un-committable non-empty project slice is preserved as a **project-only pending slice under the COMMUNE suffix, whatever the source drop's kind** — a signoff-suffix pending would be reaped by `sweep_stale_signoff` on the next listene...\n 660│- **spt-core mapping:** `spt-live/src/ingest.rs` `ingest_drops` (deferred-project preserve + `Ingested.preserved`); `route_slices` project gate unchanged (`REQ-STORE-CONTEXT-BRANCH-FILL`); kin `REQ-HAZARD-DROP-FILE-SINGLE-WRITER` (core stays the single writer — the preserve rewrite is core's own write).\n 661│- **Source:** MSG-IDENTITY W3 (doyle triage, code-grounded ingest.rs:156 gate vs :200 delete; repro fixture F-032-commune-2026-07-08T222721Z.md).\n 662│\n\n## adr/\n### 0004-single-daemon-broker-brain-split-and-self-update.md\n 10│\n*11│1. **Consolidation.** The sister project runs poll listeners as ephemeral per-session background tasks and Psyche wrappers as detached per-live-agent supervisor processes. But poll listeners already interact directly with the agent session (capsule/idle), and Psyche wrappers already invoke harness binaries directly. Once the daemon owns every PTY, keeping these as separate processes is unjustified.\n 12│\n 13│2. **Seamless self-update with a hard no-terminate constraint.** Self-update is a day-one pillar. The constraint: *no endpoint process may terminate or suspend during an spt-core update* — we cannot assume every endpoint can safely suspend. The naive \"drain + restart the daemon\" approach violates this for spt-hosted sessions (the daemon owns their PTY; killing the daemon SIGHUPs the child).\n 14│\n   │...\n 16│\n*17│**Consolidate all per-machine logic into the one daemon.** Poll-listener logic and Psyche/pulse loops move into the `spt-daemon` — no separate listener or wrapper processes. The only residue is a thin, stateless **in-session relay** for harness-hosted sessions (topology 1), where spt cannot reach into a process tree it doesn't own; the relay just pipes the daemon's events into the session's stdout and is freely killable.\n 18│\n 19│**Split the logical daemon into two implementation layers** to satisfy the no-terminate invariant:\n 20│- **broker** (stable kernel) — holds *only* un-transferable, must-not-die resources: PTY master fds, spawned harness child processes, listening network sockets. Minimal, versioned local IPC. Almost never updates.\n   │...\n 32│\n*33│- The daemon is the single brain for a machine; crash-recovery and update logic must cover PTYs, networking, registry, spools, listeners, and psyche loops together.\n 34│- A small internal broker process exists beneath the daemon — a deliberate, bounded walk-back of \"literally one process,\" preserving B1's *intent* (one network identity, one supervisor, one firewall prompt) while guaranteeing endpoint survival across updates.\n 35│- Peer-propagated updates make release signing mandatory, not optional.\n 36│- spt-core becomes the update conductor for adapters too; adapter manifests must declare an update avenue.\n### 0005-totp-seeded-spake2-node-pairing.md\n 68│  node pubkey, and a relay routes one endpoint per key), and a per-subnet\n*69│  rendezvous **meet** listener rotates with the TOTP step. There is no arming\n 70│  step: this completes the one-sided UX this ADR intended (\"a seed-holder\n*71│  node must be online and reachable\"). The standing-listener exposure is\n 72│  guarded by the #11 subnet-global rate limiter, a one-ceremony-at-a-time\n 73│  shed at the accept door, and a ceremony timeout aligned with the limiter's\n*74│  stale-slot reclaim — an attacker reaching the listener still gets one\n 75│  online guess per admitted ceremony, serialized and backed off.\n 76│- **Membership mutations are elevation-gated** (the REQ-PAIR-6 machinery,\n 77│  exit 3, gate-before-everything): `subnet create` reveals a fresh subnet's\n### 0018-broker-brain-process-isolation-restoration.md\n 36│- **Sequencing:** the restoration is the next milestone, before `spt-claude-code` scoping (operator-accepted 2026-06-09). Rationale: it is the *last* release that needs a manual fleet daemon bounce — paid 3× for v0.3.2 — so every adapter-era release rolls seamlessly; and the adapter is better built on the final topology than atop a daemon under later surgery while it hosts the user's daily driver. The split changes daemon internals, not the M8-frozen CLI/api surface.\n*37│- **Out of scope (unchanged):** the broker-touching update classes (broker-compatible / broker-breaking) remain as ADR-0004 left them; whole-daemon FD-passing stays the deferred \"future polish\" (ADR-0004:38); a durable in-daemon alarm scheduler is a separate gap (alarms are legacy-listener-only today).\n### 0020-event-envelope-sole-arriving-format-reply-to-removed.md\n  19│  adapters (\"their consumer is the adapter's hook composer, not the event\n *20│  stream\"), while the **listener stream** (`api listen` / `spt ready`) already\n  21│  renders the canonical `<EVENT>` envelope via `render_event_lines`.\n  22│\n  23│The result: the spool stores `(from_id, body)` columns → `format_row` serializes\n *24│them into the `__REPLY_TO__:` string → for the listener, `render_event_lines`\n  25│parses that string **back** into `(from, body)` to compose `<EVENT>`. A pointless\n  26│round-trip through a relic, and `api poll` hands the relic straight to adapters.\n  27│\n    │...\n  57│     single-line `<EVENT …>…</EVENT>` per message** (body `<br>`-escaped). The\n *58│     **listener stream** additionally chunks an oversized line into\n  59│     `<EVENT-PART seq=\"N/M\">` groups so the **`« spt event »` Monitor's ~500-char\n  60│     notification cap** holds (`EVENT_LINE_THRESHOLD`) — a harness-notification\n  61│     concern. The **hook drains (`api poll`/`worker-poll`) emit whole `<EVENT>`s,\n    │...\n 130│    `emit.rs::parse_frame` removed (compose from structural `(from, body)`);\n*131│    `deliver.rs` / `listener.rs` / `ring.rs` / `ready.rs` rebind; `lib.rs`\n 132│    ADR-0001 framing corrected (the `<EVENT>` envelope is the stable format, not\n 133│    the TCP frame).\n 134│  - `spt` — `api poll` / `api worker-poll` (`delivery.rs::emit`) compose `<EVENT>`\n### 0022-spt-hosted-idle-delivery-translation-binary.md\n  21│\n *22│Today there is also a coverage gap: `api bind` registers no listener port, so a listener-less\n  23│spt-hosted perch *spools* inbound messages — only spooling + adapter-poll works.\n  24│\n  25│We previously locked (Q1 → \"A+\", CCS session 16531b26, 2026-06-17) a **unified** model: the daemon's\n    │...\n  49│  spt-core owning every PTY write is what lets injection coexist with a live `spt rc` controller.\n *50│- **Poll-listener preference:** spt-core prefers a perch's poll listener if one exists, so an\n *51│  spt-hosted endpoint can run a listener AND keep `spt rc`. A dev may keep the perch \"idle-ready\"\n *52│  and use either the listener or the translation binary for *all* delivery.\n  53│- **Scope:** idle delivery only. Busy/mid-turn delivery stays adapter hook-injection.\n  54│\n  55│The v0.11.0 raw inject is the **degenerate special case** — a translation binary that emits\n    │...\n 109│- **Binary hosts its own `spt api listen` + writes the PTY directly.** Rejected: spt-core already\n*110│  owns listener logic, and if the binary wrote the PTY directly spt-core could not coordinate\n 111│  atomicity with the controller. Piping the feed to stdin and reading keystrokes from stdout keeps\n 112│  the binary trivial and keeps every PTY write under spt-core's control.\n 113│- **Keep two distinct delivery paths (Q1 option B).** Rejected in favour of the unified poll-feed\n### 0026-multi-subnet-home-at-endpoint-run-creation.md\n  6│\n *7│accepted (2026-06-23; v0.14.0 endpoint-creation-flow, W6 int keystone green — `multi_subnet_bringup_e2e` proves the multi-subnet refuse + `--subnet` bind-inherit on a ≥2-subnet node, no double-seed). Refines ADR-0010 (immutable home subnet) at the spt-hosted creation seam. Sibling of ADR-0027 (Unbound state + attach-on-session). Delivered in the endpoint-creation-flow milestone (post-v0.13.2).\n  8│\n  9│## Context\n 10│\n### 0027-unbound-endpoint-state-and-attach-on-session.md\n *1│# Unbound endpoint state + attach-on-session-exists\n  2│\n  3│<!-- [doc->REQ-ENDPOINT-UNBOUND-ATTACH] -->\n  4│\n   │...\n  6│\n *7│accepted (2026-06-23; v0.14.0 endpoint-creation-flow, W6 int keystone green — `multi_subnet_bringup_e2e` proves a fresh hold-unbound endpoint is `spt rc`-attachable pre-bind, and `picker::data::tests::gather_renders_live_unbound` proves the real on-disk→render Unbound seam). Sibling of ADR-0026 (multi-subnet home at creation). Delivered in the endpoint-creation-flow milestone (post-v0.13.2).\n  8│\n  9│## Context\n 10│\n   │...\n 18│\n*19│- **Attach-on-session-exists.** `cmd_endpoint_run` (and `spt rc <id>`) attach as soon as a **live broker session** exists for the id — regardless of perch status — so the operator sees and drives the harness immediately, including clearing a bind-gating prompt. This also makes a **headless** bringup attachable before/without bind. The source of truth is the broker `sessions` map (the per-endpoint registry, ADR-0025 W3a). This *preserves* REQ-HAZARD-RC-ATTACH-ONLINE-RACE's \"don't attach before there's...\n 20│\n 21│- **New on-disk status `STATUS_UNBOUND`.** The daemon stamps `UNBOUND` at spawn; `bind` flips it to `ONLINE`; session death → `OFFLINE`. (Chosen over a purely-derived display so the state is explicit and queryable in the daemon-authoritative liveness model.)\n 22│  - **Lifecycle reuses the existing reconcile/exit-waiter** — a dead broker session → `OFFLINE` (the same path that marks dead-but-latched perches offline). No new teardown; `UNBOUND` auto-resolves (→`ONLINE` on bind, →`OFFLINE` on session death).\n   │...\n 35│- A new on-disk liveness status (`STATUS_UNBOUND`) — the liveness predicates and reconcile/teardown must treat it honestly (attachable, not message-addressable; auto-resolves).\n*36│- `spt rc <id>` works against an unbound endpoint (a live session with no bind) — useful for headless bringups and for clearing a bind-gating prompt.\n 37│- The attach gate moves earlier (session-exists), removing the prompt-before-bind deadlock while keeping the original race protection.\n 38│- `EpDisplay` grows (hollow + hollow-controlled); the picker/legend gain the Unbound state.","linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784197397381}}
{"type":"message","id":"82f68c09","parentId":"d682b013","timestamp":"2026-07-16T10:24:15.151Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Assessing elevated process communication methods**\n\n**Planning privilege-independent integration tests**\n\n**Designing Windows elevation A/B tests**"},{"type":"thinking","thinking":"**Designing elevated and unelevated process interaction**\n\n**Tracing elevated endpoint ownership and attachment limits**\n\n**Verifying messaging feasibility via elevated relay**"},{"type":"toolCall","id":"call_pjapYT8wxUFKlyKepA8xul4t|fc_017bff223290b7ba016a58b14f1d1c819abba5be87ac360515","name":"read","arguments":{"i":"Reading endpoint bind lifecycle","path":"crates/spt/src/api/startup.rs","selector":"110-230,261-470,472-810"}},{"type":"toolCall","id":"call_uHpMBuvDQmU93psCmnIvWSZO|fc_017bff223290b7ba016a58b14f1d30819a8deff5a4452cde9c","name":"read","arguments":{"i":"Reading listener registry lifecycle","path":"crates/spt-msg/src/listener.rs","selector":"1-41,90-180,180-240"}},{"type":"toolCall","id":"call_d4jZ4nQh6KkkrIOSreAsQ4Vc|fc_017bff223290b7ba016a58b14f1d38819aa944f37440654737","name":"read","arguments":{"i":"Reading local send production path","path":"crates/spt/src/cli.rs","selector":"5630-5805"}},{"type":"toolCall","id":"call_YymgFwnXn2VWJKPL9IXkif2L|fc_017bff223290b7ba016a58b14f1d54819aa895a5d090c5a833","name":"read","arguments":{"i":"Reading RC attachment path","path":"crates/spt/src/rc.rs","selector":"1-220"}},{"type":"toolCall","id":"call_xs2KTgUaNp6qZGDmrRNKqjn7|fc_017bff223290b7ba016a58b14f1d5c819a89190e2739c31d9b","name":"read","arguments":{"i":"Reading WAN receiving path","path":"crates/spt-daemon/src/msg.rs","selector":"1-260"}},{"type":"toolCall","id":"call_bA4fkq3A6VSXw4ESGMrAOiFc|fc_017bff223290b7ba016a58b14f1d64819a9d26473edf5bbbc9","name":"read","arguments":{"i":"Reading endpoint metadata schemas","path":"crates/spt-store/src/info.rs","selector":"20-230,620-675"}},{"type":"toolCall","id":"call_pvneLNsgNnfURSwNTznYUjWQ|fc_017bff223290b7ba016a58b14f1d68819aa284cddd39f8b18c","name":"read","arguments":{"i":"Reading network registry schema","path":"crates/spt-net/src/net/registry.rs","selector":"1-260"}},{"type":"toolCall","id":"call_3UvmdOzFUK7Bw8BhxodX6uD2|fc_017bff223290b7ba016a58b14f1d70819a9a277bc6dc9e4c4e","name":"read","arguments":{"i":"Reading relevant architecture decisions","path":"docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md","selector":"1-220"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":30253,"output":1334,"cacheRead":82432,"cacheWrite":0,"totalTokens":114019,"reasoningTokens":1006,"cost":{"input":0.151265,"output":0.04002,"cacheRead":0.041215999999999996,"cacheWrite":0,"total":0.232501}},"stopReason":"toolUse","timestamp":1784197397434,"responseId":"resp_017bff223290b7ba016a58b115c010819ab6914772270fb7f1","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b12922b4819ab897eefc6d187e5c","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLE3T7-aWvAHTPYnOK_jVhG8_jOaHrpdfNGliLwtPVbHVSfhvZ1XEFFpHDrNApwsgNss6EKonaAtEJQp4DI4C-mCJzfMVrrl-c1zOTEwFEs2aevyGMY-AD-_qiojWGyCLnFaFsu_GMirdUwJL8In9l0HWfU_l1ViPam_Vf-hZIG1xmLGEn8_Bx_ZxCkVnX-vhuw1-tipMwO-zxLJa8O784SB_qmS-GS-l7hqxcoI1yMWDPFZoFp6crrLJfZnxmj96xwu_wJ8VYfsTGYSuJPTq1A4GC9ajorHDm7lRgzTFMyT1yOBwpuX-MZOBMGcuy0EasM7qs2BZKDwesJhOW8fG_V5tdRNj5cNF91vcucRnxfgjT-CY1g7ywOTeTu0loSYXF0vyl5Wp4hCK_tNAvw4lJnKJbKZzdUfqkojK6WxGsZlVVpjqitmE9N5Hom4utvP-geDF_QS4-P3s2bhkz3i9O_b4tupy4rnSEvtvgjcwNpz022PhwrxqIUdAZRd8zKvIfJehhXiroksFTyYdLtDENBnY5eJ2ickqH0uzkA2OOqX5Dy9fpxcoY9v_K2W21L2Dz2gYuIorlR8RKUpYL4drAxjSyRnrFvO8JSP-jY-hbH1En4hvn9sxNZuxVf1dQfhC5P20OUCbSZgJb1RuiF3af3KHatpWQ-krDMoJOU9FIGqh_NMPq4FnokXkdsZ0OJTomyUZPkqSz8MNYs6obtGuVPlOzxLCFQwbW_2U9e7M0AZkEeDX-2AQ5lOfuShWyT0Ecnj6IjM5JxMhmDQIZF8qBjmeCY215CC4U8OIM4c27iJ7PLlU3K27er1XTKMzkdxQxSaVtOJXf_l57m32ThbbDl2CGdA7K1GhXLxDLmxLLQE-1kGKExTpAZtUXq-9sOlwUwEUPUqq1gZafq383Qb4Dik9wqSIKMUZ492Zw6F87SATpqvWja_kJG9qc8_fDtLauze_91UYYtVSd8sa1x4yxb0DwDhenInmvQy6Uh4eMqr00km7xP3KAou6U9mFEzT-MLfaYmkM3EDGeN2NWzTezpxXUBBc2PHrxi8mzHPhy-0zQ4McGvW-_BYu4hbLcvTNvFWSSVQabw50AZES7D99AHePHdLTXxYX91EKyveD2At4lwI4rsPaFdgFdrBZyBcKVprG-qpB12dYXbiRfymWlXfLSe8Aij7-E2YGrvLSnpkamK5rL8Hurj0d3A5tQNZyP95PbLneQyzZ3YCEu8gCZCuJd7RAm-lpk7C9EpR-s-KjkoCosDodhjScdTjMwsri9zjtZ-_4pn2bzY2gYjm14Jvcsyupb1P-MzCfRE-ts7yoqblEYuO9ReV0_eyR0tAZcm9wFsoKslCyF_5u8q3ifN6emf1LREUG076zZoMqhtZD2V0C9WsXpMDiiS4guqAQnfw9xnQHtVh-bKEw0Gwcwjhb6Jbkc3Kxejb7Q3QBAaSukpxmChTXTOwxFfBdlw1mIcW0uvBT5mNUVq3-mbYkdQwTc9bGldAnSV3CvF63GBnX9H9sBVZzpIw77KoGEgXt1CHd0q_MT9r4mSTEpH3GpVMKEocB0yusI3nSobX_XYIaoLzDfwXhfpbDMrlzrpMKhQNLagja7g5bWcoGKgn1oBk2zBz001uIqYu31iZJ0vopJF0mefjnnfiTKvxO0clGkWcY0qbkiVi5Tlree7ilDle_HZgQgcuHLbXAgJZQD4AWzt-_l6WxfXaDLhc4tLxj7-sJ3-vWDULaEDJG_Az9GVz_bWeyZYzIOns10QccqNEY3LiSWgWzFWUBQaMxWyUiroNf9F_PZMv9tW_FGJeOJpQn45AcnmXjlB6DMvKJ6tuAtK6wPCRrsGGGc9Edoti7lG4_K8Bg3qaU5MT_WbdTvVG3vVaZUAwySbJQpuEHHmUY9x91YBouLI0xYcInhjH04hb3cQMcEyDUzKxUVLOHK-OjDczSq4HWAIxlcEG4D3kzK9X0JF8UTyNGFt_6d-d_CTUiFKJoWq7QV0KX2gCP5gLlHZO4fYMgMh7nhmSsw_cV3cE3P1M-VEmQ0UBfhsWhhgh9hZceqhsF6E0V8UEeJgsFJVVttVX-PdFDiJvUvqhJv5US4z2glEa-ibpQwdgEIq5amVW2RPAYPic-DUnIYNMm7KbDQwZWY7IN_UhlH3ylFEu4rdKvff0KnRtA7ErQzUBKkEtSUuJrPV-ozAQCM3WPcrxyTOUWqtNs9QzdZGNJsq_Rbjwmq_RQ8mVEwvV-t78rs6z9Ou1szVIrxB239NRPjstkdntqJ_wVzrt4cuhlj_PuOgFkLRj4_JhgjsK8Ywh-fLTSUSm2M7q9L2O1Y3mW9qlQsU87SQbHWkdL_UaXszCFBzlAsiWV4PUAUd7GfKQcAmk_XyN6dMfzpuTMAgalvDx8Do2RFnFmIamCSMWmf1h-18pHPekGeBL9MT5dnPZBYV39KeHE-4Wn5zI8Lv2aexaA5na9BhX9jYK1uVRFaxW6plMIeGOqhBmS3KOz5QH5WY_M6iLBsyH_AxXBLpsEKLGlzBolsj8fzZCgh7Onucj1GIAgPY3Q7RKsQ-3NbAKkXyTVa__Cb-IFKjUe0rD5Xtbv79GSctQGuaeQIXAFHcyvzUItjvDqN_iXUSzHKzHf8I5Ueea2gMFHeLUFHnQW63Q217ey_x5pnUNC6MML3UerkDzxamrvRt1AKn-UQceEJ5BxcwpurGT1_5ghdBaLVgxVu-Nh5KdZuEDyHsxxs4b7Gl6XoH6G6A-UBnSvXO-6gPMYiGDWlymTrgTfEtXWFgE_r9ijFJ65fY1lUa0nfMKuU4pyE1ohFRvworUm3pMw5VGm8B_dSq5cysMxo2xY8mU67neQr5CrY2Q9AonMhh60jrVaCJ9hfGj8UCXqu7Kwj-LoZBSiLxdNip-zBQJy9Fi_RBmZL3jgpvKm8Ob-V-jCcOU9t5hP4-ywLpYZwa9K5tMAKwQElS0vSQHhR34fz8xABquCdaBqAwcagIfLKfOAFOdLW9q4y3L8j-uzsI3O_c83Lq0VwT149S5BGLTd70RY2v0zx0doHTy3ysEeCCySug9lcZyYjZ9j6iuUg1XogkDjmpsN8iRCDHQsVte3jear3jyrrbfEF9wZqnhZWpJ4AhWBbh9Uzaw89_D_sCNXVRX2srynjUS3kOubUqvYvyd2QM_-Ec_KBbqft6cj_57CF-xheuAaKRFK7Gka8iTFfsa_eM2a-KNbIM9dhI27UxQA3BSnaoCsfIWoJumvB1EbpKsS_d1T-tOobrue371Zp2uFw5916F-kkToH6PQ04UqkpfdzRhuLqtO4zbjC-zGmpo5GZ3EAQ3459ibPHWibc4JfFaPaveTnKaPcTR9nXKgj7-oLl6lTuaA3RZDZEQkAVX7Z8aoMaGRbmDGt_Ln-t4XclGdBWsdzbO3iS6gnjwA0ziFJhWZS9GXkniLFcHvYW3oNwiRsYgSo8CCF6kWedmFKPt3mqxVVvGaa_uIigDF6DmdZET08nLqyR6U34pK4jQqztw7IKYkGl0Pcc9AntKYhxDDTe29I3J7GuzOzUxDJY7esfbElTnjyHcEi-OS2YHz_jtpQhmHRff5GbL-6Cjd5OLhhxZnhPZT_q59PZFIGrn3yk9_gEHSOTLqJYa-UCBY75UUL3Y8LQZGnOe8LCg_1Iw6hhWvwl2jMoYWxgCiqXf6BFjJ8EuQx-WdRvy30GW4URV2fPUEOuRf0C8b2RvmrWDbND7qWZ8H-ba3a-1Gg1HJ2upNpRyYQRFgVGF8bJLBJx9fTV5bloKwC5nXz5y2oz4D38bMp97uz0gi_nGTsXL8GMesFS7IBaQVlYVyk3t0e9HZrILw7sz0FpH1HjEqiafS996xnx-N05IQJNeLrl82yxATAHLiPpc00uxeVTypGyHPbDo8bnvCWk7Lnu2sQoXIpx1MDtDMcWEryqNNCKmQN24uYxTG6YWtC0Rousz5OnJJPADqmpRZ3ZvpTZenY_5nYeNmNNsqbDX3NHzg3ocj5JHHDciKPcKeNgZ1brXa_keImDChDmYbbfeHtXJL6gGEkB5rgSOILYias_Stj6ivrdpuF-MDDMeSk-_rGDCMlR7lXNpnrciEoCXZuhXedyY9jpX0_hDX6CziPVZjb-yXsdYtS6rSA39rjTs3J_lKL9U_q9YZqnDVyYxg5RshXKoWLNuDORdsxm2kGy402tbpwXdM83RLya_mW02pnz_sId7wadADU4toeMy8NZJ1Ntm1rvkhe4Z3TJDPCghxgrIs_9pE4DK7bSE8-kJcAmFJ9lfp9y6htbJtl5DbJ8NeWVWyJWS3yNJSHl1YQld-OMmVCFQA60b4CRWgm6M15Iz68mLmdNUUzz5EC99owyHWNi4puxEeen4AG8Y3Y_5x6ACu6_9OP5jFy26unIzZ5rHoBGcxqUneJYsqr7j8MjGLCT0jEfczSrvoZ9LBb-wuzFuLc8hllrd7nMDywUa4LgUZxc6bfoMxFiin_y207KzNHarjPza3ZL0nsvXWgXKrjDxLo05dpiQPObbwcn6YcgIcmZ6A9il7aval9dGzTiNWp6ZbHAnPXhcRXJ2oiXdPSlUNqyoiWkj1tIJnZXE=","summary":[{"type":"summary_text","text":"**Assessing elevated process communication methods**"},{"type":"summary_text","text":"**Planning privilege-independent integration tests**"},{"type":"summary_text","text":"**Designing Windows elevation A/B tests**"}]},{"id":"rs_017bff223290b7ba016a58b137b1d0819aa0bf95a60d64cabc","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLFPFFVG-_ggqy_WdBxKTW_eWWzFkPcepLizAE3E45XHnYDAveKd0jAJQyZcY24czaDLZ_Wz73aAwTcFgy0XQzg9E0iLed5-CfDTZbH8CS1dyMVM3udJXoCjpwLU_4Qx_U6TZL0j0h_sNkF8m1OImBmjPVVpBIUveMQkrTf2P3wcaMaUckvEl6p9H91PlOZmbJuE4AdzimhxqWuJq96Lc6NYsEfMtHQ3OMr60d02JZa2Tenaz10kXS6lO1BZ_NzmoAHwhjiV9x1-0VBNDUYOzzEODT_nkbBpcioVRh5XWyhDItmWzuVoM9hzkelarHEpSekYvxOWtuzYQyNE7h2UEMD0iLOzBpYR2oA7qOSEhqKUE4veLo0FN5cA5A5tl03SL3ytpQcM6M0KgWgV5j4es93tGLamDW5jX3uZBiRzwbEJ7zN_wjykQLfkbrp6QTUi1H7ZCQ4vs931z7we0v8bLYu3ea66ojNfrDW3i7gbx2kfeQBeCrI0xUcOdAivBjStrmWxzJ3etvs_V5tyUQ5olfArE5fQXMqZNRYNW6TSfjtldfgCsYc4HAtb7zbN3WAB1Z3x7xrGZynkJ_Rulc58HCp1QWdI6hNPTlBZNlbrzdpbekf3RZvzZDklpr0tY3vk1z5HcxVFccNXyta2ckEElHbbZ6i5sJl8wtkMOl5YhRBj6KXeMrwg_4zhuKXL0PbR-1sAH1dgazfkriQCzEWSzI7HaYydlXGluza-fo0YgyPj7ipIaV8jIWyRcB1LWQKHkoLBARM7e58GRO9BOdcmILdtxuPIQX7Pz28uoEVjdgv4BbETvoqgfmAWx8NAYcOTriRSn0wEu-YoQzTcNXMtFMKr59L4TD1Y1XoNxyd0uhG_Yt_hdKtZjYjqZQ0FWVifTJ3uVR88B3rWCYbca02VbtfyZUwMTqUDSN4MP022wErVi66TccHgFcoY2-NQTjb-wMaPgt72iZAyV3GdfyGoa9XxDjj6ceH7CsMSilC8Na6iUr0aK0Uri-qCdFKxQYxlWayYY3rf1yXXibIL2xh2qtNPWk4279KYwzX6Oq2RVpwor34pWpYHSaSsXUqiEQ2bT2Zp79W8kGdznY2kRaNc8nUoxkP4FQje9g17eyHZdYndJZCcPZuqSRvVVoA_lwyLumI1m7PUBr_setNClQVnHOLry9fI5Jy4wE8lF6SqDdMlYEVraIttoo31vNKJibK2l2uGMwfUVyLvgzIz3xZy4IwUqdoi4oBNvgzjRmJqNjbb_jhXXBvPOI8UuY0Erhy0yxSNDPLi3BekWA4yVVGyWhro1jBnbaATr2t0tj6JFq3hhjeWD-IlWmhbAmInb_pBxAqi5ogYfJSPiU81OAAkmB68GI2nFeE3wO9Zi1Cs_GXT6lSgvPu6zJ4euGFVlLLLXEPa7FanwTZb6-yeAo1pp2W7RIn-35aUgd0NHNVSQkFKHeFkhivPWFlaNKTcoiEf1G9K7fqe4DWGVU8lMSUuYPQzBcxiG6XfAi4lUxToV2Yqnp4skxyiUP7-Ft5ZJs6L6XI4San6-CMXoFZ_lP6lEZr-Fh2wTxenN0AMqnwj-Ps0fNwUuCMXWXMh5zKirH06W4zNH35dY2m2tiaVTq-foqrGwro4Qm6kSbNfKeTIhgeXqjRpsJhpWPSnNicXMULt_QzTa2wk8E-nNaT1iWjizAd2_tfocHUWfaASsxqVvX_Vq57Om_pYG-1C9ZkRXXpFcVDVVbWoFUEV-CZIguFPvmxW3NBD3B5DoT5ija7BrhywyCK7QbBpeKkqswI2D_zTlEUTclgnvAX37Pdha7ZN_gcz_8Em90cpy8AhSKeVRl2Ijs-V-efaxzHykK6iNdivznzI2TQBlIJMJWCeeC9OpY9NsJixSKaPsEL-WiFPzqzeeDRyXUAD5fB_O0fhzRo7wK3uXsgt60vGliJD4a6CiNb8gdOKexre-AirJ346Dw14ZDXSVbZh7z3T6-mUMFxHNlnIhUxuAxSqZI05nV-rET3woEcW_bxsW1mRcKq0rp701URgxeK4dMNm9_O4W1QGchh3k9a_oyLWycwX6_go4hYU7v1isGmbfuJpiR3iEp6m2tNXJCphjU-bJSvGhvCNEsDa7TcrheSb-eelZ24cJ954lciqe1d7DkMuNc2nSJaPxNVfSikkpnwcCDGLlyTX9dbt_K9Wp9XGYHGWe3c_6Qe3dWYSwUVhNY5zG857EoIKGdNxCgyEF7p6cYRYhkPZRzCr84WUVoP2IYxcRgmsy08dN03tRKZhTiJDQafoijh11Jy_GrV9H3pzb9YBRKD_fz9cA5FmWVhTbVSFB8aFLAYoTs1suuc0i7laRuMwm0AD2iAyvJ2zIhVgyGpoLmxKHe07AVeJZJhx_zxYsIrzUG9E3U4UmNCNYBHuWG_EXqEGi3mDhYMUQyQGu5rzVRtQwsc8SFkXx98g7dH9idltHFSMwKc0NWL4Fhchq8KiZMdHQddFkjwdBbigbOlga6dsqhUjUDtRghce9Mm6Dr50LwpcMI_Inbg8E8M6BsRe58Rw0POmR7USnhSgsXSlOlP0aLz_Xp4bfAn3lzsxt82K8NWA2KQ5PvZP2mp6uQarp0-98UtMzR9ua06TC2BcdkjKdxC6mu8aIOzE_EJY-_YmzM_-RDZRbn48wFPTPoTzwKSJpL02KTdtLNyjDWuJV1FJmUKEa45bOtwfmxQCalMogZ2CZIlNDuYpT3AeUzi-3rW5VBVBOfjkS4pWygPxs71QsfHp8mNIeemnm5-dvIjgGShnSMRk6P7ZYLjcj7zI9ZYGS1J63S0CNL-JD5gz0c0MklbZl73eLu5tLBlNuyi7psxlIc58hCtKW855GGyAjcJ6yCsZpP21BJXlWXKGxg0Vcm4xWLde-5yuZ0YNREec_7w0Sk3QznmfEEqQ9ZiN-oeE61IK9maXVx_hkrNA5LrN1x5V2piYHPn7lkVh2czLJ5i2I89D6E58YzyDJ0uhUmiqNxC9YfRKToja3hWorbc6Y76d6GfwM3wEqWMtc_joedfk-jJYoY5VTtGmqmfDUO6GPkNpYaE6oyYubpk8zblIWgw22r26xBCTG9jSX6zskoSqTQUHSI_t92-SNJxjDBpHxyxqOjTix0w86f4aisnOKemaBoPZ1QNKCu2pHK9IphG6s_XvYwLrQnfWdtZ1BxUvAg_v6UGCgkgJgZG-iHKfWZ-iYffNtRqq8-Km94sTOkE1nwH78piiwRrGI-wToJElv65SpzQE2ARo4EPhnXRW2_lKu2XGnggRYg16JmGkc8nyuo46V2uIW3g_3adEQ4ZaepdPMI-y6TwCMLCxC8lvxashVjnchTMV1sUdr4TlW7tX9DFEhmJPAVhoMtGDtR7PmUgnDJQlCgB9MXIc4Y49zW_kiCZ9hPzTppJHJVIqmGD9F4xozdqk0-TeXmspc6HWilwEF4HpNLM0x-EriWt_xjUc3CHJRCTw8U3lzuwwvw6flhtgqWCm03zmwD09iARp1AQ_sAQs43x-bJVRTBURjDesx32tpepTBAzpBn2CEZgFB2EKrIEZBNF_wXm_Yel4fadM2J3uZl-8rqB739wwAO7B-Gi9Jg2G2eyab5Lz7qSM8V1oi2tO0-m9XZgUC8XvbhhfQl1C1nynZgyUxISQhiC2XgyzUVj4chvmKsw4YmYcPNNlkve81FTWZc9qgFC2gDXcR-709hQvmQlV_lz2s7sDdbQR9hI7EmYW7iZg3mMyjMqxCwkb3QFxCZI9VHsuVQBhZX1hnPWnVtxMndB88BQ69ijt7WaHXlhGNtc4-UAJvKuv28lrrL2WIGgviXJRPXAYpgZOBb0IvBSdPGag7wvfMnedSUDxYxxW1FH9QctuupR6Ia0kkEYCKrsI9y182BMfWvq4jj9rYhvvJRrsVexMNWrY9Z_XMEJCzPFqSGuSt8AcVb0reL3k1e41kEn_4DMc5Bcwlg4sfnKBpYYUPaOrWFNrA1BL6V6dWWgb4IAzUkbbQGeW0BvVwLx_S1AwkrzHX8On_wi2budgLqHSCI-G6O4rt8faY4Ll211qVGP1RLf-tfYb0SMveufxqtEYfM5tbwUIVvIbkgs-9TPbjACx","summary":[{"type":"summary_text","text":"**Designing elevated and unelevated process interaction**"},{"type":"summary_text","text":"**Tracing elevated endpoint ownership and attachment limits**"},{"type":"summary_text","text":"**Verifying messaging feasibility via elevated relay**"}]},{"id":"fc_017bff223290b7ba016a58b14f1d1c819abba5be87ac360515","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading endpoint bind lifecycle\",\"path\":\"crates/spt/src/api/startup.rs\",\"selector\":\"110-230,261-470,472-810\"}","call_id":"call_pjapYT8wxUFKlyKepA8xul4t","name":"read"},{"id":"fc_017bff223290b7ba016a58b14f1d30819a8deff5a4452cde9c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading listener registry lifecycle\",\"path\":\"crates/spt-msg/src/listener.rs\",\"selector\":\"1-41,90-180,180-240\"}","call_id":"call_uHpMBuvDQmU93psCmnIvWSZO","name":"read"},{"id":"fc_017bff223290b7ba016a58b14f1d38819aa944f37440654737","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading local send production path\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"5630-5805\"}","call_id":"call_d4jZ4nQh6KkkrIOSreAsQ4Vc","name":"read"},{"id":"fc_017bff223290b7ba016a58b14f1d54819aa895a5d090c5a833","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading RC attachment path\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1-220\"}","call_id":"call_YymgFwnXn2VWJKPL9IXkif2L","name":"read"},{"id":"fc_017bff223290b7ba016a58b14f1d5c819a89190e2739c31d9b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading WAN receiving path\",\"path\":\"crates/spt-daemon/src/msg.rs\",\"selector\":\"1-260\"}","call_id":"call_xs2KTgUaNp6qZGDmrRNKqjn7","name":"read"},{"id":"fc_017bff223290b7ba016a58b14f1d64819a9d26473edf5bbbc9","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading endpoint metadata schemas\",\"path\":\"crates/spt-store/src/info.rs\",\"selector\":\"20-230,620-675\"}","call_id":"call_bA4fkq3A6VSXw4ESGMrAOiFc","name":"read"},{"id":"fc_017bff223290b7ba016a58b14f1d68819aa284cddd39f8b18c","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading network registry schema\",\"path\":\"crates/spt-net/src/net/registry.rs\",\"selector\":\"1-260\"}","call_id":"call_pvneLNsgNnfURSwNTznYUjWQ","name":"read"},{"id":"fc_017bff223290b7ba016a58b14f1d70819a9a277bc6dc9e4c4e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading relevant architecture decisions\",\"path\":\"docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md\",\"selector\":\"1-220\"}","call_id":"call_3UvmdOzFUK7Bw8BhxodX6uD2","name":"read"}]},"duration":57717.04889999982,"ttft":19673.72269999981,"contextSnapshot":{"promptTokens":112685,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pjapYT8wxUFKlyKepA8xul4t|fc_017bff223290b7ba016a58b14f1d1c819abba5be87ac360515","toolName":"read","startedAt":"2026-07-16T10:24:15.152Z","args":{"path":"crates/spt/src/api/startup.rs"},"intent":"Reading endpoint bind lifecycle"},"id":"24cb8607","parentId":"82f68c09","timestamp":"2026-07-16T10:24:15.152Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_uHpMBuvDQmU93psCmnIvWSZO|fc_017bff223290b7ba016a58b14f1d30819a8deff5a4452cde9c","toolName":"read","startedAt":"2026-07-16T10:24:15.152Z","args":{"path":"crates/spt-msg/src/listener.rs"},"intent":"Reading listener registry lifecycle"},"id":"40bdeb47","parentId":"24cb8607","timestamp":"2026-07-16T10:24:15.152Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_d4jZ4nQh6KkkrIOSreAsQ4Vc|fc_017bff223290b7ba016a58b14f1d38819aa944f37440654737","toolName":"read","startedAt":"2026-07-16T10:24:15.152Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading local send production path"},"id":"0f344b4c","parentId":"40bdeb47","timestamp":"2026-07-16T10:24:15.152Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_YymgFwnXn2VWJKPL9IXkif2L|fc_017bff223290b7ba016a58b14f1d54819aa895a5d090c5a833","toolName":"read","startedAt":"2026-07-16T10:24:15.152Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading RC attachment path"},"id":"9cacc45e","parentId":"0f344b4c","timestamp":"2026-07-16T10:24:15.152Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xs2KTgUaNp6qZGDmrRNKqjn7|fc_017bff223290b7ba016a58b14f1d5c819a89190e2739c31d9b","toolName":"read","startedAt":"2026-07-16T10:24:15.152Z","args":{"path":"crates/spt-daemon/src/msg.rs"},"intent":"Reading WAN receiving path"},"id":"5ad9f1ec","parentId":"9cacc45e","timestamp":"2026-07-16T10:24:15.152Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_bA4fkq3A6VSXw4ESGMrAOiFc|fc_017bff223290b7ba016a58b14f1d64819a9d26473edf5bbbc9","toolName":"read","startedAt":"2026-07-16T10:24:15.152Z","args":{"path":"crates/spt-store/src/info.rs"},"intent":"Reading endpoint metadata schemas"},"id":"99a7511c","parentId":"5ad9f1ec","timestamp":"2026-07-16T10:24:15.152Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_pvneLNsgNnfURSwNTznYUjWQ|fc_017bff223290b7ba016a58b14f1d68819aa284cddd39f8b18c","toolName":"read","startedAt":"2026-07-16T10:24:15.152Z","args":{"path":"crates/spt-net/src/net/registry.rs"},"intent":"Reading network registry schema"},"id":"f9cac70d","parentId":"99a7511c","timestamp":"2026-07-16T10:24:15.152Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_3UvmdOzFUK7Bw8BhxodX6uD2|fc_017bff223290b7ba016a58b14f1d70819a9a277bc6dc9e4c4e","toolName":"read","startedAt":"2026-07-16T10:24:15.152Z","args":{"path":"docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md"},"intent":"Reading relevant architecture decisions"},"id":"0b337200","parentId":"f9cac70d","timestamp":"2026-07-16T10:24:15.152Z"}
{"type":"message","id":"b123eb42","parentId":"0b337200","timestamp":"2026-07-16T10:24:15.165Z","message":{"role":"toolResult","toolCallId":"call_pjapYT8wxUFKlyKepA8xul4t|fc_017bff223290b7ba016a58b14f1d1c819abba5be87ac360515","toolName":"read","content":[{"type":"text","text":"pub enum BindError {\n…\n    PsycheCustodySquat {\n        id: String,\n        psyche: String,\n    },\n    Io(String),\n}\n\n/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.\n/// Pure of any listener I/O so it is unit-testable without spawning processes.\n/// Returns the perch's capability token on success.\n///\n/// REQ-START-3, refined by the F-034 leg-b split (doyle gate ruling, PR #80):\n/// a seed is SPENT (stays taken) by a successful bind AND by any refusal that\n/// proves the seed itself dead — a STALE anchor or an EMPTY session can never\n/// retry as itself, and restoring a dead-anchor seed would re-arm it keyed to\n/// a dead pid for a RECYCLED pid to re-take and bind under the old victim's\n/// session id (the exact REQ-HAZARD-WINDOWS-PID-RECYCLE vector the spend\n/// defense exists to close; the harness re-seeds on its next SessionStart).\n/// A RECOVERABLE refusal (HOME_REFUSED, a live conflict, a custody-squat —\n/// the anchor still alive) RESTORES the seed so the corrected retry on the\n/// SAME pid binds instead of dead-ending on NO_SEED\n/// (effect-before-irreversible-consume, kin F-032).\n// [impl->REQ-START-2]\n// [impl->REQ-START-3]\n// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]\n// [impl->REQ-START-5]\npub fn bind_from_seed(\n    id: &str,\n    parent_pid: u32,\n    subnet: Option<&str>,\n    adapter: Option<&str>,\n) -> Result<String, BindError> {\n    let seed = take_seed_from_daemon(parent_pid)?.ok_or(BindError::NoSeed(parent_pid))?;\n    // [impl->REQ-LISTEN-SEED-CONSUME-AFTER-BIND]\n    let result = bind_taken_seed(id, &seed, subnet, adapter);\n    if result.as_ref().is_err_and(seed_restorable) {\n        // Best-effort: the bind refusal stays the primary diagnostic; a failed\n        // restore only degrades to the old consumed-seed behavior — loudly.\n        if let Err(e) = spt_daemon::put_seed(&spt_daemon::seed_socket_name(), &seed) {\n            eprintln!(\"SEED_RESTORE_FAIL:{parent_pid}: {e}\");\n        }\n    }\n    result\n}\n\n/// Whether a post-take refusal leaves the seed RESTORABLE (the F-034 leg-b\n/// spend-vs-restore split, doyle-ruled): restore only when the anchor is still\n/// alive to retry — a dead-anchor (`StaleSeed`) or contentless (`EmptySession`)\n/// seed is SPENT on refusal, closing the recycled-pid re-take vector\n/// (REQ-HAZARD-WINDOWS-PID-RECYCLE). `NoSeed` never reaches here (nothing was\n/// taken).\n// [impl->REQ-LISTEN-SEED-CONSUME-AFTER-BIND]\nfn seed_restorable(e: &BindError) -> bool {\n    !matches!(e, BindError::StaleSeed(_) | BindError::EmptySession)\n}\n\n/// The post-take half of [`bind_from_seed`]: validate the taken seed and bind.\n/// Split out so the caller can restore the seed on ANY refusal here\n/// (consume-only-on-success — REQ-LISTEN-SEED-CONSUME-AFTER-BIND).\nfn bind_taken_seed(\n    id: &str,\n    seed: &Seed,\n    subnet: Option<&str>,\n    adapter: Option<&str>,\n) -> Result<String, BindError> {\n    if seed.session_id.is_empty() {\n        return Err(BindError::EmptySession);\n    }\n    // Pid-recycle defense (5.1): anchor liveness on the stable parent pid. A\n    // dead anchor means the harness that wrote the seed is gone — a recycled or\n    // abandoned seed must never bind.\n    if !proc::is_process_alive(seed.parent_pid) {\n        return Err(BindError::StaleSeed(seed.parent_pid));\n    }\n    // The seed is adapter-agnostic (ADR-0021): the creating adapter is the one\n    // the caller resolved at bind (explicit --adapter override, else the\n    // host_binaries/pointer resolver), not a seed-time snapshot. An empty option\n    // stamps no adapter — the same backward-compatible degrade as before.\n    let adapter = adapter.filter(|a| !a.is_empty());\n    // The relay/listen path is the agent host — always an agent-family type.\n    // (A non-agent endpoint, e.g. a Gateway, establishes via `api bind --type`,\n    // not the live-agent relay loop.) Harness-hosted: the harness owns the process,\n    // so there is no broker PTY → not controllable (REQ-PICKER-1).\n    establish_perch(\n        id,\n        &seed.session_id,\n        seed.parent_pid,\n        adapter,\n        subnet,\n        \"live_agent\",\n        Some(false),\n        // W3 (REQ-HAZARD-BIND-CWD-UNSET): the harness session's cwd was captured at\n        // `api seed` and, until now, DISCARDED here — thread it through so a\n        // harness-hosted endpoint records its project dir.\n        seed.cwd.as_deref(),\n    )\n}\n\n/// Bind WITHOUT a seed, from an explicit session id (F-034 leg c): a session\n/// that goes live LATE (hours after SessionStart, or after a daemon restart)\n/// finds its ephemeral seed gone — nothing re-fires it until the NEXT\n/// SessionStart — so `listen --session-id <sid>` binds directly.\n///\n/// Same GATES as the seed path — NOT the same provenance (doyle gate ruling,\n/// PR #80): a seed is a consume-once capability the harness minted for exactly\n/// one anchor pid; `--session-id` is a BEARER STRING — anyone who knows a live\n/// sid can present it, so session-id secrecy is a security invariant (a known\n/// live sid can revive that perch). Accepted local-surface posture: the\n/// `--parent-pid` override already permits a local squat, so this stays a\n/// doc+test qualification, not a redesign. The gates themselves hold exactly\n/// as on a seeded bind: the parent-pid anchor must be ALIVE (5.1 pid-recycle\n/// defense), `establish_perch` refuses a live-owner conflict, and the\n/// psyche-custody squat guard refuses a psyche's own custody sid. The cwd is\n/// this process's own (the listen alias runs inside the harness session's\n/// shell), normalized like `cmd_seed`'s capture.\n// [impl->REQ-LISTEN-SESSION-ID-FALLBACK]\nfn bind_from_session_id(\n    id: &str,\n    session_id: &str,\n    parent_pid: u32,\n    subnet: Option<&str>,\n    adapter: Option<&str>,\n) -> Result<String, BindError> {\n…\n}\n…\n\n/// Write the perch record (skeleton→live) + ready marker + capability token.\n/// Reviving an existing perch keeps its token; a live perch held under a\n/// different session id is a conflict (pid-recycle / id collision guard).\n///\n/// Creation-time fields ride the shared seam\n/// ([`spt_store::home::stamp_creation_fields`], REQ-INST-15): a NEW perch\n/// assigns its immutable home (refuse-and-qualify on a multi-subnet node) +\n/// seeds `sync_subnets = [home]`; a revive **carries forward** home, the\n/// adapter (an explicit one wins — resume-under-new), the resource blurb,\n/// and the auto-suspend override — a re-bind rewrites `info.json` and must\n/// not wipe them.\n// [impl->REQ-INST-15]\n// W3 added `cwd`, bringing this private bind helper to 8 params (> clippy's 7).\n// A params struct is the cleaner shape but a heavier refactor across both call\n// sites — deferred to a follow-up; allow the lint on this internal fn for now.\n#[allow(clippy::too_many_arguments)]\nfn establish_perch(\n    id: &str,\n    session_id: &str,\n    parent_pid: u32,\n    adapter: Option<&str>,\n    subnet: Option<&str>,\n    endpoint_type: &str,\n    controllable: Option<bool>,\n    cwd: Option<&str>,\n) -> Result<String, BindError> {\n    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n\n    // Psyche-custody squat guard (F-036 leg c, MSG-IDENTITY W1): a bind whose\n    // sid equals ANY nested psyche's OWN custody sid is refused before anything\n    // is touched — core owns psyche-custody.json, so the collision is visible\n    // right here at bind time. The identity-env sanitize (leg a) removes the\n    // known leak vector; this guard makes the seat-theft CLASS unreachable (a\n    // psyche never binds a perch — its custody record is deliberately not an\n    // instance, FORK-2 — so a custody-sid bind is wrong by definition).\n    // [impl->REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD]\n    if let Some(psyche) =\n        spt_store::psyche_custody::custody_squatter(&perch::owlery_dir(), session_id)\n    {\n        return Err(BindError::PsycheCustodySquat {\n            id: id.to_string(),\n            psyche,\n        });\n    }\n\n    // The perch dir must exist before the CAS (the `.info.lock` sentinel lives in it).\n    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;\n\n    // Nested-resolution seam (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): a NESTED id\n    // (Psyche/Worker) derives its home from the PARENT perch it lives under, so a\n    // child-side bind needs no `--subnet` the child can't know. Explicit still wins; a\n    // Self id falls through to assign_home's auto/refuse policy unchanged. The impure\n    // parent read lives HERE at the caller boundary (before the locked build), keeping\n    // stamp_creation_fields pure.\n    // [impl->REQ-PSYCHE-NESTED-RESOLUTION]\n    let eff_subnet = spt_store::home::effective_subnet(id, subnet, &perch::owlery_dir());\n\n    // The prior-read + conflict-check + record-build + write run as ONE critical\n    // section under the per-perch `.info.lock` (REQ-HAZARD-INFO-RMW-LOST-UPDATE): an\n    // unlocked check-then-write here could interleave a concurrent daemon `mutate_info`\n    // stamp and be silently clobbered — the record renamed in, then the RMW writing back\n    // its stale pre-bind snapshot + a status stamp, leaving a pre-bind shape that reads\n    // ONLINE (counter-39 #2's dead-but-ONLINE surface via a bind race). `establish_locked`\n    // reads `prior` under the lock and holds it through the write.\n    let build = |prior: Option<InfoJson>| -> Result<InfoJson, BindError> {\n        if let Some(existing) = &prior {\n            let owner_alive = info::read_pid(&perch_path)\n                .map(proc::is_process_alive)\n                .unwrap_or(false);\n            if owner_alive && !existing.session_id.is_empty() && existing.session_id != session_id\n            {\n                return Err(BindError::Conflict {\n                    id: id.to_string(),\n                    held: existing.session_id.clone(),\n                });\n            }\n        }\n        // The endpoint type tag (info.json `state`). Defaults to the agent path\n        // (`live_agent`); a non-agent endpoint (e.g. a `gateway` — the open type\n        // system, REQ-EP-1/REQ-EP-6) binds with its own tag via `api bind --type`.\n        // [impl->REQ-EP-6]\n        // Open by construction: any tag round-trips through EndpointType::from_tag,\n        // and downstream type-keyed logic (the user-msg identity gate, REQ-MSG-5)\n        // reads it back verbatim. A revive keeps the prior type unless overridden.\n        let endpoint_type = prior\n            .as_ref()\n            .map(|p| p.state.clone())\n            .filter(|s| endpoint_type == \"live_agent\" && !s.is_empty())\n            .unwrap_or_else(|| endpoint_type.to_string());\n        let mut rec = InfoJson::new(\n            id,\n            &now_stamp(),\n            std::process::id(),\n            session_id,\n            &endpoint_type,\n        );\n        rec.parent_pid = Some(parent_pid);\n        let subnets = spt_store::subnet::SubnetStore::load();\n        let mut vis = spt_store::visibility::VisibilityStore::load();\n        let dirty = spt_store::home::stamp_creation_fields(\n            &mut rec,\n            prior.as_ref(),\n            &subnets,\n            &mut vis,\n            eff_subnet.as_deref(),\n            adapter,\n        )\n        .map_err(|e| BindError::Home(e.to_string()))?;\n        if dirty {\n            let _ = vis.save();\n        }\n        // Broker-PTY-controllability stamp (REQ-PICKER-1): the caller knows the hosting\n        // mode — `listen` is harness-hosted (no broker PTY → Some(false)); `bind` of a\n        // live_agent is spt-hosted (broker PTY → Some(true)); a non-live bind is None.\n        // Explicit wins; a revive that omits it carries the prior value forward (like\n        // home/adapter — a re-bind must not wipe it).\n        // [impl->REQ-PICKER-1]\n        rec.controllable = controllable.or_else(|| prior.as_ref().and_then(|p| p.controllable));\n        // W3 (REQ-HAZARD-BIND-CWD-UNSET): record the working directory so the picker\n        // can group this endpoint under its PROJECT (data.rs → project_id_for_dir).\n        // Previously NEVER set on bind — the refuted v0.12.1 \"fresh agent shows under\n        // its project\" P1. spt-hosted: cmd_bind passes its own current_dir (the broker\n        // spawned it in project_cwd); harness-hosted: bind_from_seed passes the seed's\n        // captured cwd. A revive carries the prior cwd forward (a re-bind must not wipe\n        // it), like home/adapter/controllable.\n        // [impl->REQ-HAZARD-BIND-CWD-UNSET]\n        rec.cwd = cwd\n            .map(str::to_string)\n            .or_else(|| prior.as_ref().and_then(|p| p.cwd.clone()));\n        // Capture the adapter's manifest-declared `[env] direction = \"read\"` vars from\n        // THIS bind's launch environment (REQ-DIGEST-PROFILE-ENV). Bind is the only\n        // point the profile env is present in-process (harness-hosted: `api listen`\n        // runs inside the harness under its ccs profile; spt-hosted: `api bind` runs in\n        // the broker-spawned harness wrapper that inherited the spawn env). The\n        // on-demand `[digest]` extractor runs LATER in the daemon context where that env\n        // is gone, so a profile-relocated transcript root (e.g. ccs's\n        // `CLAUDE_CONFIG_DIR`) must be persisted here to resolve then. Only DECLARED\n        // read-vars, only when set (an allowlist). A revive with none freshly present\n        // carries the prior capture forward (like cwd/adapter — a re-bind must not wipe\n        // a good value).\n        // [impl->REQ-DIGEST-PROFILE-ENV]\n        rec.read_env = adapter\n            .and_then(|a| spt_runtime::registry::resolve_option(&perch::adapters_dir(), a).ok())\n            .map(|(_, m)| spt_runtime::runtime::capture_read_env(&m, |k| std::env::var(k).ok()))\n            .filter(|captured| !captured.is_empty())\n            .or_else(|| {\n                prior\n                    .as_ref()\n                    .map(|p| p.read_env.clone())\n                    .filter(|c| !c.is_empty())\n            })\n            .unwrap_or_default();\n        // DEFECT A (REQ-HAZARD-BIND-REST-STATE-CARRY): a re-bind must NOT wipe the\n        // daemon-owned resting intent (D9-2, REQ-INST-3). `InfoJson::new` defaults\n        // `rest_state` to `None`, so a fresh record from a re-bind drops a\n        // `suspended`/`dormant` wake intent (flynn's tick11 `rest_state:active`\n        // vanish) — the SAME carry-forward discipline as cwd/controllable/read_env.\n        // Carry the dormant anchor with it so a carried `dormant` keeps its\n        // auto-suspend counting anchor (`dormant_since_ms` is present iff dormant).\n        // [impl->REQ-HAZARD-BIND-REST-STATE-CARRY]\n        rec.rest_state = prior.as_ref().and_then(|p| p.rest_state.clone());\n        rec.dormant_since_ms = prior.as_ref().and_then(|p| p.dormant_since_ms);\n        Ok(rec)\n    };\n    let rec = match info::establish_locked(&perch_path, build) {\n        Ok(inner) => inner?,\n        Err(io) => return Err(BindError::Io(io.to_string())),\n    };\n    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\n        .map_err(|e| BindError::Io(e.to_string()))?;\n\n    // Session ledger (REQ-TERM-6): record this session as active so the digest\n    // can span a later `/clear`|`/compact` boundary. The boot row dedups against\n    // a same-session revive; a missing/unstamped session is not ledgered (the\n    // digest spans only when there is a session to enumerate). Best-effort — a\n    // ledger problem must never fail the bind.\n    if !session_id.is_empty() {\n        let _ = spt_store::sessions::append(\n            &perch_path,\n            &spt_store::sessions::SessionEntry {\n                ts: spt_store::timefmt::now_rfc3339(),\n                session_id: session_id.to_string(),\n                trigger: spt_store::sessions::SessionTrigger::Boot,\n                // Record the bind cwd (W3 `rec.cwd`, just written above) so a\n                // later picker Resume of this boot session restores its project\n                // dir (REQ-SESSION-RESUME-TEMPLATE).\n                cwd: rec.cwd.clone(),\n                // `append` stamps the persisted monotonic ordinal (REQ-DIGEST-CURSOR).\n                ordinal: None,\n                // Record the adapter this boot session runs under so a later\n                // Resume restores the same harness (D-2, REQ-SESSION-ADAPTER-RECORDED).\n                // [impl->REQ-SESSION-ADAPTER-RECORDED]\n                adapter: rec.adapter.clone(),\n            },\n        );\n        // New session/cwd → refresh this endpoint's project-index row (+ the\n        // shared cwd cache for the bind cwd) — ADR-0037 §3. Fire-and-forget.\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n        spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n            id: id.to_string(),\n            cwd: rec.cwd.clone(),\n        });\n    }\n\n    // Keep an existing token across a revive; mint one for a fresh perch.\n    let token = auth::read_token(&perch_path).unwrap_or_else(auth::mint_token);\n    auth::write_token(&perch_path, &token).map_err(|e| BindError::Io(e.to_string()))?;\n    Ok(token)\n}\n…\n/// `api listen <id>` — consume the seed, bind the perch, then run the relay\n/// loop (heir to `$LIVE start`'s Monitor-bound poll loop). `--once` drains and\n/// exits (testability); otherwise blocks streaming messages to stdout.\n///\n/// When the manifest is **live-capable** (declares `[session.psyche_init]`), the\n/// loop also hosts the LiveAgent lifecycle over the M2b `spt-live` seams: sweep\n/// stale signoff on start (3.2), spawn the Psyche (T2), and run a pulse tick\n/// each cycle (drop ingest + echo fires — T3/T4/T5). The graceful signoff (T6)\n/// is driven by the shutdown path; M3's daemon re-hosts all of it.\n// [impl->REQ-START-2]\n// [impl->REQ-SEAM-PSYCHE]\n/// How often the listen-orphan watchdog polls `--parent-pid` liveness\n/// (REQ-HAZARD-LISTEN-ORPHAN). A headless-gateway orphan's failure is \"held ONLINE for\n/// minutes\", so a seconds-latency poll is a full fix (doyle W4 ruling 2: POLL baseline,\n/// one code path both platforms; OS-native immediacy — Win job object / Linux PDEATHSIG\n/// — is an OPTIONAL follow-up). The listener exits within one of these windows.\nconst PARENT_WATCH_POLL: std::time::Duration = std::time::Duration::from_secs(2);\n\n/// Process exit code when the listener self-exits because its `--parent-pid` host died\n/// (REQ-HAZARD-LISTEN-ORPHAN) — distinct from a clean `0` or a refusal, so a log/operator\n/// can tell an orphan-exit from a normal end.\nconst EXIT_PARENT_GONE: i32 = 3;\n\n/// One watchdog-tick decision (REQ-HAZARD-LISTEN-ORPHAN): the listener must exit once its\n/// `--parent-pid` anchor is no longer alive. Pure over [`proc::is_process_alive`] so the\n/// exit trigger is unit-testable without a real host fork.\n// [impl->REQ-HAZARD-LISTEN-ORPHAN]\nfn parent_is_gone(parent_pid: u32) -> bool {\n    !proc::is_process_alive(parent_pid)\n}\n\n/// The loud orphan-exit line — names this listener + the dead parent, so a false-ONLINE\n/// perch becomes a diagnosable exit, never a silent hang. Pure for the unit.\n// [impl->REQ-HAZARD-LISTEN-ORPHAN]\nfn listen_orphan_exit_line(id: &str, parent_pid: u32) -> String {\n    format!(\n        \"LISTEN_ORPHAN_EXIT:{id}: parent pid {parent_pid} is gone — exiting the orphaned \\\n         listener so the perch flips OFFLINE (its recorded pid dies) and dead-owner rebind \\\n         unblocks [REQ-HAZARD-LISTEN-ORPHAN]\"\n    )\n}\n\n/// Spawn the listen-orphan watchdog (REQ-HAZARD-LISTEN-ORPHAN). `--parent-pid` is the\n/// auth anchor, but with no liveness watch a host death orphans the listener forever: the\n/// perch stays falsely ONLINE, EVENTs stream to a dead stdout, and dead-owner rebind is\n/// BLOCKED (the recorded pid is the live orphan). This bounded poll exits the process LOUD\n/// within one [`PARENT_WATCH_POLL`] window on parent death, so the perch's recorded pid\n/// dies → liveness flips it OFFLINE → rebind unblocks. Persistent-relay branch ONLY (a\n/// `--once` shot exits immediately — no orphan window). flynn's spt-mobile-side job-object\n/// guard stays as independent defense-in-depth (doyle W4 ruling 2); this is the core fix.\n// [impl->REQ-HAZARD-LISTEN-ORPHAN]\nfn spawn_parent_watchdog(id: &str, parent_pid: u32) {\n    let id = id.to_string();\n    std::thread::spawn(move || loop {\n        std::thread::sleep(PARENT_WATCH_POLL);\n        if parent_is_gone(parent_pid) {\n            eprintln!(\"{}\", listen_orphan_exit_line(&id, parent_pid));\n            std::process::exit(EXIT_PARENT_GONE);\n        }\n    });\n}\n\npub fn cmd_listen(\n    id: &str,\n    parent_pid_override: Option<u32>,\n    once: bool,\n    adapter: Option<&str>,\n    manifest: Option<&spt_runtime::Manifest>,\n    subnet: Option<&str>,\n    session_id: Option<&str>,\n) -> i32 {\n    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.\n    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {\n        eprintln!(\"INVALID_ID:{id}: {e}\");\n        return EXIT_REFUSED;\n    }\n    let parent_pid = match parent_pid_override.or_else(proc::parent_pid) {\n        Some(p) => p,\n        None => {\n            eprintln!(\"NO_PARENT_PID: could not resolve the parent pid anchor\");\n            return EXIT_REFUSED;\n        }\n    };\n\n    // Bind-time adapter/profile resolution (ADR-0021 / REQ-START-5): an explicit\n    // `--adapter` is the override; omitted, resolve the owning adapter from the\n    // seed's parent pid (exe basename → host_binaries candidates → active-profile\n    // pointer / registered_at_ms). A zero-match is a friendly refusal naming the\n    // binary + the `--adapter` escape — never a silent bind without an adapter\n    // (which would drop the Psyche, the very /sptc:live bug class).\n    // [impl->REQ-START-5]\n    let adapters_dir = spt_store::perch::adapters_dir();\n    let adapter_option: Option<String> = match adapter {\n        Some(a) => Some(a.to_string()),\n        None => match spt_runtime::resolve::resolve_bind_adapter(&adapters_dir, parent_pid) {\n            Ok(opt) => Some(opt),\n            Err(e) => {\n                eprintln!(\"ADAPTER_UNRESOLVED:{id}: {e}\");\n                return EXIT_REFUSED;\n            }\n        },\n    };\n    // The effective manifest: an explicit one (resolved in `run` from\n    // `--adapter`/`--manifest`), else the resolved adapter's registered manifest —\n    // the source of `live_capable` (psyche_init) + endpoint-native notif render.\n    let resolved_manifest: Option<spt_runtime::Manifest> = match manifest {\n        Some(m) => Some(m.clone()),\n        None => adapter_option.as_deref().and_then(|opt| {\n            spt_runtime::registry::resolve_option(&adapters_dir, opt)\n                .ok()\n                .map(|(_, m)| m)\n        }),\n    };\n    let manifest = resolved_manifest.as_ref();\n\n    let token = match bind_from_seed(id, parent_pid, subnet, adapter_option.as_deref()) {\n        Ok(t) => t,\n        // Late go-live / post-daemon-restart (F-034 leg c): the ephemeral seed\n        // is gone and nothing re-fires it until the NEXT SessionStart, so an\n        // explicit --session-id binds directly. NO_SEED only — any OTHER\n        // refusal keeps its own diagnostic (the sid fallback must never paper\n        // over a stale anchor, a live conflict, or a HOME refusal). Loud\n        // distinct marker so a field log shows which path bound.\n        // [impl->REQ-LISTEN-SESSION-ID-FALLBACK]\n        Err(BindError::NoSeed(_)) if session_id.is_some_and(|s| !s.is_empty()) => {\n            let sid = session_id.expect(\"guarded by the match arm\");\n            match bind_from_session_id(id, sid, parent_pid, subnet, adapter_option.as_deref()) {\n                Ok(t) => {\n                    eprintln!(\"SID_BIND:{id}: no live seed — bound from --session-id\");\n                    t\n                }\n                Err(e) => {\n                    report_bind_error(&e);\n                    return EXIT_REFUSED;\n                }\n            }\n        }\n        Err(e) => {\n            report_bind_error(&e);\n            return EXIT_REFUSED;\n        }\n    };\n    eprintln!(\"BOUND:{id} token={token}\");\n\n    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n    let relay = match spt_daemon::relay::Relay::bind(id, &perch_path) {\n        Ok(r) => r,\n        Err(e) => {\n            eprintln!(\"LISTEN_FAIL:{id}: {e}\");\n            return EXIT_REFUSED;\n        }\n    };\n\n    // Live-agent FIRST-HOST handoff to the daemon (M11-W0.2, REQ-DAEMON-1): the\n    // Psyche/pulse lifecycle is no longer hosted in THIS process — it lives in the\n    // brain (`spt_daemon::livehost`). For a live-capable endpoint (manifest\n    // declares `[session.psyche_init]`) we mark the perch `status=online` at\n    // establish; the brain's reconcile then hosts the Psyche within one tick (the\n    // shellwake CLI-flips-status / brain-reconciles pattern). `status` is\n    // daemon-authoritative (CONTEXT:194). A new `listen` against a not-yet-restarted\n    // OLD brain leaves the perch online-but-unhosted; that self-heals at the brain's\n    // next BOOT reconcile (livehost reconciles at boot, not only on tick) — the\n    // agent + the message relay below are live throughout regardless.\n    // [impl->REQ-DAEMON-1]\n    let live_capable = manifest.is_some_and(|m| m.session.psyche_init.is_some());\n    if live_capable {\n        if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {\n            eprintln!(\"LIVE_ONLINE_FAIL:{id}: {e}\");\n        }\n    }\n\n    eprintln!(\"READY:{id}\");\n\n    // New-session-start is a reported resurface boundary (ADR-0007/D8b):\n    // undismissed notifs surface into the spool before the backlog drain, so\n    // they ride this very first drain — and render endpoint-native when the\n    // manifest declares [session.notif]. Best-effort — never fails the listen.\n    super::reporting::resurface_notifs_with(id, manifest);\n\n    // Deliver one message to the live agent and record it as an spt context\n    // injection for the two-origin digest (REQ-TERM-7) — `emit` is the\n    // agent-facing chokepoint, so every delivered message becomes a glanceable\n    // `owl_message` context entry. The recorded form is the whole composed\n    // `<EVENT>` (what the agent sees), not the structural pair. Best-effort;\n    // never disturbs delivery.\n    let mut deliver = |from: &str, body: &str| {\n        emit(from, body);\n        let event = spt_msg::emit::render_event_whole(from, body);\n        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, &event);\n    };\n\n    if once {\n        // Single-shot (testability): drain the offline backlog (incl the just-\n        // resurfaced notifs) then forward one live delivery, and exit.\n        relay.drain_backlog(&mut deliver);\n        relay.pump_once(&mut deliver);\n        return 0;\n    }\n    // Listen-orphan watchdog (REQ-HAZARD-LISTEN-ORPHAN): watch the `--parent-pid` host\n    // and exit LOUD if it dies, so this listener never orphans the perch falsely ONLINE\n    // (EVENTs to a dead stdout, dead-owner rebind blocked). Persistent branch only.\n    // [impl->REQ-HAZARD-LISTEN-ORPHAN]\n    spawn_parent_watchdog(id, parent_pid);\n\n    // The pure relay loop (M11-W0.2): `Relay::run` drains the backlog first\n    // (preserving the resurface→drain ordering) then streams live deliveries to\n    // stdout until the process is killed. No lifecycle state lives in this process\n    // anymore — the Psyche/pulse runs in the brain. The stop flag is never raised;\n    // the harness kills the process to end the session (or the watchdog exits it on\n    // parent death).\n    let stop = std::sync::atomic::AtomicBool::new(false);\n    relay.run(&stop, &mut deliver);\n    0\n}\n\n/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the\n/// live perch on first contact, or flips an existing skeleton, keyed to the\n/// session id the binary reports. Auth is intrinsic: an existing *live* perch\n/// under a different session id is refused (you can only bind your own).\n// [impl->REQ-SEAM-POSTSPAWN]\npub fn cmd_bind(\n    id: &str,\n    set_session_id: Option<String>,\n    proof_session: Option<String>,\n    adapter: Option<&str>,\n    subnet: Option<&str>,\n    endpoint_type: &str,\n    manifest: Option<&spt_runtime::Manifest>,\n) -> i32 {\n    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.\n    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {\n        eprintln!(\"INVALID_ID:{id}: {e}\");\n        return EXIT_REFUSED;\n    }\n    let session_id = set_session_id\n        .or(proof_session)\n        .or_else(|| std::env::var(\"OWL_SESSION_ID\").ok())\n        .filter(|s| !s.is_empty());\n    let Some(session_id) = session_id else {\n        eprintln!(\"USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)\");\n        return EXIT_REFUSED;\n    };\n    // The session's own anchor: the process that fired this hook (the session).\n    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);\n    // spt-hosted: the daemon spawned a live_agent into a broker-owned PTY → it IS\n    // broker-PTY-controllable (REQ-PICKER-1). A non-live bind (a gateway/shell — not\n    // a PTY-attachable agent) stamps None; the picker's type-gated derivation never\n    // amber-paints it regardless.\n    let controllable = if endpoint_type == \"live_agent\" {\n        Some(true)\n    } else {\n        None\n    };\n    // W3 (REQ-HAZARD-BIND-CWD-UNSET): the broker spawned this spt-hosted bind\n    // subprocess in the endpoint's PROJECT cwd (REQ-HOST-RUN-2), so our own\n    // current_dir IS the project dir — record it on the perch.\n    let cwd = std::env::current_dir()\n        .ok()\n        .map(|p| p.to_string_lossy().into_owned());\n    match establish_perch(\n        id,\n        &session_id,\n        parent_pid,\n        adapter,\n        subnet,\n        endpoint_type,\n        controllable,\n        cwd.as_deref(),\n    ) {\n        Ok(token) => {\n            eprintln!(\"BOUND:{id} token={token}\");\n            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)\n            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen\n            // mark. A live_agent endpoint whose manifest declares\n            // `[session.psyche_init]` is marked status=online at establish; the\n            // brain's livehost reconcile then hosts the Psyche (the SAME single host\n            // path — bind never spawns/hosts/calls livehost). Closes the\n            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so\n            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct\n            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so\n            // the establish-online signal == the reconcile host predicate (no\n            // online-but-unhostable perch). status is daemon-authoritative (CONTEXT:194).\n            // [impl->REQ-DAEMON-1]\n            let live_capable = endpoint_type == \"live_agent\"\n                && manifest.is_some_and(|m| m.session.psyche_init.is_some());\n            if live_capable {\n                let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n                if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {\n                    eprintln!(\"LIVE_ONLINE_FAIL:{id}: {e}\");\n                }\n            }\n            // A post-spawn bind is a new-session-start — a reported resurface\n            // boundary (ADR-0007/D8b). Best-effort.\n            super::reporting::resurface_notifs(id);\n            0\n        }\n        Err(e) => {\n            report_bind_error(&e);\n            EXIT_REFUSED\n        }\n    }\n}\n\nfn report_bind_error(e: &BindError) {\n    match e {\n        BindError::NoSeed(p) => eprintln!(\"NO_SEED:{p} (no seed for this parent pid)\"),\n        BindError::StaleSeed(p) => eprintln!(\"STALE_SEED:{p} (anchor process is dead)\"),\n        BindError::EmptySession => eprintln!(\"EMPTY_SESSION: seed carried no session id\"),\n        BindError::Conflict { id, held } => {\n            eprintln!(\"CONFLICT:{id} is live under session {held}\")\n        }\n        BindError::Home(m) => eprintln!(\"HOME_REFUSED: {m}\"),\n        BindError::PsycheCustodySquat { id, psyche } => eprintln!(\n            \"PSYCHE_CUSTODY_SQUAT:{id} refused — the requested session id is psyche \\\n             {psyche}'s own custody sid (a real perch must never pin to a psyche sid)\"\n        ),\n        BindError::Io(m) => eprintln!(\"BIND_IO: {m}\"),\n    }\n}\n\n/// Emit one delivery to the listener stream: compose the structural\n/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass\n/// through; oversized lines chunk into EVENT-PART) and print + flush. One\n/// delivery per emit.\n// [impl->REQ-MSG-4]\n// [impl->REQ-MSG-ENVELOPE]\nfn emit(from: &str, body: &str) {\n    use std::io::Write;\n    for line in spt_msg::emit::render_event_lines(from, body) {\n        println!(\"{line}\");\n    }\n    let _ = std::io::stdout().flush();\n}\n\n/// Coarse creation timestamp (epoch seconds as a string), matching the\n/// `info.json` `started` producer-format convention.\nfn now_stamp() -> String {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_secs().to_string())\n        .unwrap_or_else(|_| \"0\".to_string())\n}"}],"details":{"displayContent":{"text":"pub enum BindError {\n…\n    PsycheCustodySquat {\n        id: String,\n        psyche: String,\n    },\n    Io(String),\n}\n\n/// Consume the seed for `parent_pid` and bind/revive the perch skeleton→live.\n/// Pure of any listener I/O so it is unit-testable without spawning processes.\n/// Returns the perch's capability token on success.\n///\n/// REQ-START-3, refined by the F-034 leg-b split (doyle gate ruling, PR #80):\n/// a seed is SPENT (stays taken) by a successful bind AND by any refusal that\n/// proves the seed itself dead — a STALE anchor or an EMPTY session can never\n/// retry as itself, and restoring a dead-anchor seed would re-arm it keyed to\n/// a dead pid for a RECYCLED pid to re-take and bind under the old victim's\n/// session id (the exact REQ-HAZARD-WINDOWS-PID-RECYCLE vector the spend\n/// defense exists to close; the harness re-seeds on its next SessionStart).\n/// A RECOVERABLE refusal (HOME_REFUSED, a live conflict, a custody-squat —\n/// the anchor still alive) RESTORES the seed so the corrected retry on the\n/// SAME pid binds instead of dead-ending on NO_SEED\n/// (effect-before-irreversible-consume, kin F-032).\n// [impl->REQ-START-2]\n// [impl->REQ-START-3]\n// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]\n// [impl->REQ-START-5]\npub fn bind_from_seed(\n    id: &str,\n    parent_pid: u32,\n    subnet: Option<&str>,\n    adapter: Option<&str>,\n) -> Result<String, BindError> {\n    let seed = take_seed_from_daemon(parent_pid)?.ok_or(BindError::NoSeed(parent_pid))?;\n    // [impl->REQ-LISTEN-SEED-CONSUME-AFTER-BIND]\n    let result = bind_taken_seed(id, &seed, subnet, adapter);\n    if result.as_ref().is_err_and(seed_restorable) {\n        // Best-effort: the bind refusal stays the primary diagnostic; a failed\n        // restore only degrades to the old consumed-seed behavior — loudly.\n        if let Err(e) = spt_daemon::put_seed(&spt_daemon::seed_socket_name(), &seed) {\n            eprintln!(\"SEED_RESTORE_FAIL:{parent_pid}: {e}\");\n        }\n    }\n    result\n}\n\n/// Whether a post-take refusal leaves the seed RESTORABLE (the F-034 leg-b\n/// spend-vs-restore split, doyle-ruled): restore only when the anchor is still\n/// alive to retry — a dead-anchor (`StaleSeed`) or contentless (`EmptySession`)\n/// seed is SPENT on refusal, closing the recycled-pid re-take vector\n/// (REQ-HAZARD-WINDOWS-PID-RECYCLE). `NoSeed` never reaches here (nothing was\n/// taken).\n// [impl->REQ-LISTEN-SEED-CONSUME-AFTER-BIND]\nfn seed_restorable(e: &BindError) -> bool {\n    !matches!(e, BindError::StaleSeed(_) | BindError::EmptySession)\n}\n\n/// The post-take half of [`bind_from_seed`]: validate the taken seed and bind.\n/// Split out so the caller can restore the seed on ANY refusal here\n/// (consume-only-on-success — REQ-LISTEN-SEED-CONSUME-AFTER-BIND).\nfn bind_taken_seed(\n    id: &str,\n    seed: &Seed,\n    subnet: Option<&str>,\n    adapter: Option<&str>,\n) -> Result<String, BindError> {\n    if seed.session_id.is_empty() {\n        return Err(BindError::EmptySession);\n    }\n    // Pid-recycle defense (5.1): anchor liveness on the stable parent pid. A\n    // dead anchor means the harness that wrote the seed is gone — a recycled or\n    // abandoned seed must never bind.\n    if !proc::is_process_alive(seed.parent_pid) {\n        return Err(BindError::StaleSeed(seed.parent_pid));\n    }\n    // The seed is adapter-agnostic (ADR-0021): the creating adapter is the one\n    // the caller resolved at bind (explicit --adapter override, else the\n    // host_binaries/pointer resolver), not a seed-time snapshot. An empty option\n    // stamps no adapter — the same backward-compatible degrade as before.\n    let adapter = adapter.filter(|a| !a.is_empty());\n    // The relay/listen path is the agent host — always an agent-family type.\n    // (A non-agent endpoint, e.g. a Gateway, establishes via `api bind --type`,\n    // not the live-agent relay loop.) Harness-hosted: the harness owns the process,\n    // so there is no broker PTY → not controllable (REQ-PICKER-1).\n    establish_perch(\n        id,\n        &seed.session_id,\n        seed.parent_pid,\n        adapter,\n        subnet,\n        \"live_agent\",\n        Some(false),\n        // W3 (REQ-HAZARD-BIND-CWD-UNSET): the harness session's cwd was captured at\n        // `api seed` and, until now, DISCARDED here — thread it through so a\n        // harness-hosted endpoint records its project dir.\n        seed.cwd.as_deref(),\n    )\n}\n\n/// Bind WITHOUT a seed, from an explicit session id (F-034 leg c): a session\n/// that goes live LATE (hours after SessionStart, or after a daemon restart)\n/// finds its ephemeral seed gone — nothing re-fires it until the NEXT\n/// SessionStart — so `listen --session-id <sid>` binds directly.\n///\n/// Same GATES as the seed path — NOT the same provenance (doyle gate ruling,\n/// PR #80): a seed is a consume-once capability the harness minted for exactly\n/// one anchor pid; `--session-id` is a BEARER STRING — anyone who knows a live\n/// sid can present it, so session-id secrecy is a security invariant (a known\n/// live sid can revive that perch). Accepted local-surface posture: the\n/// `--parent-pid` override already permits a local squat, so this stays a\n/// doc+test qualification, not a redesign. The gates themselves hold exactly\n/// as on a seeded bind: the parent-pid anchor must be ALIVE (5.1 pid-recycle\n/// defense), `establish_perch` refuses a live-owner conflict, and the\n/// psyche-custody squat guard refuses a psyche's own custody sid. The cwd is\n/// this process's own (the listen alias runs inside the harness session's\n/// shell), normalized like `cmd_seed`'s capture.\n// [impl->REQ-LISTEN-SESSION-ID-FALLBACK]\nfn bind_from_session_id(\n    id: &str,\n    session_id: &str,\n    parent_pid: u32,\n    subnet: Option<&str>,\n    adapter: Option<&str>,\n) -> Result<String, BindError> {\n…\n}\n…\n\n/// Write the perch record (skeleton→live) + ready marker + capability token.\n/// Reviving an existing perch keeps its token; a live perch held under a\n/// different session id is a conflict (pid-recycle / id collision guard).\n///\n/// Creation-time fields ride the shared seam\n/// ([`spt_store::home::stamp_creation_fields`], REQ-INST-15): a NEW perch\n/// assigns its immutable home (refuse-and-qualify on a multi-subnet node) +\n/// seeds `sync_subnets = [home]`; a revive **carries forward** home, the\n/// adapter (an explicit one wins — resume-under-new), the resource blurb,\n/// and the auto-suspend override — a re-bind rewrites `info.json` and must\n/// not wipe them.\n// [impl->REQ-INST-15]\n// W3 added `cwd`, bringing this private bind helper to 8 params (> clippy's 7).\n// A params struct is the cleaner shape but a heavier refactor across both call\n// sites — deferred to a follow-up; allow the lint on this internal fn for now.\n#[allow(clippy::too_many_arguments)]\nfn establish_perch(\n    id: &str,\n    session_id: &str,\n    parent_pid: u32,\n    adapter: Option<&str>,\n    subnet: Option<&str>,\n    endpoint_type: &str,\n    controllable: Option<bool>,\n    cwd: Option<&str>,\n) -> Result<String, BindError> {\n    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n\n    // Psyche-custody squat guard (F-036 leg c, MSG-IDENTITY W1): a bind whose\n    // sid equals ANY nested psyche's OWN custody sid is refused before anything\n    // is touched — core owns psyche-custody.json, so the collision is visible\n    // right here at bind time. The identity-env sanitize (leg a) removes the\n    // known leak vector; this guard makes the seat-theft CLASS unreachable (a\n    // psyche never binds a perch — its custody record is deliberately not an\n    // instance, FORK-2 — so a custody-sid bind is wrong by definition).\n    // [impl->REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD]\n    if let Some(psyche) =\n        spt_store::psyche_custody::custody_squatter(&perch::owlery_dir(), session_id)\n    {\n        return Err(BindError::PsycheCustodySquat {\n            id: id.to_string(),\n            psyche,\n        });\n    }\n\n    // The perch dir must exist before the CAS (the `.info.lock` sentinel lives in it).\n    std::fs::create_dir_all(&perch_path).map_err(|e| BindError::Io(e.to_string()))?;\n\n    // Nested-resolution seam (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): a NESTED id\n    // (Psyche/Worker) derives its home from the PARENT perch it lives under, so a\n    // child-side bind needs no `--subnet` the child can't know. Explicit still wins; a\n    // Self id falls through to assign_home's auto/refuse policy unchanged. The impure\n    // parent read lives HERE at the caller boundary (before the locked build), keeping\n    // stamp_creation_fields pure.\n    // [impl->REQ-PSYCHE-NESTED-RESOLUTION]\n    let eff_subnet = spt_store::home::effective_subnet(id, subnet, &perch::owlery_dir());\n\n    // The prior-read + conflict-check + record-build + write run as ONE critical\n    // section under the per-perch `.info.lock` (REQ-HAZARD-INFO-RMW-LOST-UPDATE): an\n    // unlocked check-then-write here could interleave a concurrent daemon `mutate_info`\n    // stamp and be silently clobbered — the record renamed in, then the RMW writing back\n    // its stale pre-bind snapshot + a status stamp, leaving a pre-bind shape that reads\n    // ONLINE (counter-39 #2's dead-but-ONLINE surface via a bind race). `establish_locked`\n    // reads `prior` under the lock and holds it through the write.\n    let build = |prior: Option<InfoJson>| -> Result<InfoJson, BindError> {\n        if let Some(existing) = &prior {\n            let owner_alive = info::read_pid(&perch_path)\n                .map(proc::is_process_alive)\n                .unwrap_or(false);\n            if owner_alive && !existing.session_id.is_empty() && existing.session_id != session_id\n            {\n                return Err(BindError::Conflict {\n                    id: id.to_string(),\n                    held: existing.session_id.clone(),\n                });\n            }\n        }\n        // The endpoint type tag (info.json `state`). Defaults to the agent path\n        // (`live_agent`); a non-agent endpoint (e.g. a `gateway` — the open type\n        // system, REQ-EP-1/REQ-EP-6) binds with its own tag via `api bind --type`.\n        // [impl->REQ-EP-6]\n        // Open by construction: any tag round-trips through EndpointType::from_tag,\n        // and downstream type-keyed logic (the user-msg identity gate, REQ-MSG-5)\n        // reads it back verbatim. A revive keeps the prior type unless overridden.\n        let endpoint_type = prior\n            .as_ref()\n            .map(|p| p.state.clone())\n            .filter(|s| endpoint_type == \"live_agent\" && !s.is_empty())\n            .unwrap_or_else(|| endpoint_type.to_string());\n        let mut rec = InfoJson::new(\n            id,\n            &now_stamp(),\n            std::process::id(),\n            session_id,\n            &endpoint_type,\n        );\n        rec.parent_pid = Some(parent_pid);\n        let subnets = spt_store::subnet::SubnetStore::load();\n        let mut vis = spt_store::visibility::VisibilityStore::load();\n        let dirty = spt_store::home::stamp_creation_fields(\n            &mut rec,\n            prior.as_ref(),\n            &subnets,\n            &mut vis,\n            eff_subnet.as_deref(),\n            adapter,\n        )\n        .map_err(|e| BindError::Home(e.to_string()))?;\n        if dirty {\n            let _ = vis.save();\n        }\n        // Broker-PTY-controllability stamp (REQ-PICKER-1): the caller knows the hosting\n        // mode — `listen` is harness-hosted (no broker PTY → Some(false)); `bind` of a\n        // live_agent is spt-hosted (broker PTY → Some(true)); a non-live bind is None.\n        // Explicit wins; a revive that omits it carries the prior value forward (like\n        // home/adapter — a re-bind must not wipe it).\n        // [impl->REQ-PICKER-1]\n        rec.controllable = controllable.or_else(|| prior.as_ref().and_then(|p| p.controllable));\n        // W3 (REQ-HAZARD-BIND-CWD-UNSET): record the working directory so the picker\n        // can group this endpoint under its PROJECT (data.rs → project_id_for_dir).\n        // Previously NEVER set on bind — the refuted v0.12.1 \"fresh agent shows under\n        // its project\" P1. spt-hosted: cmd_bind passes its own current_dir (the broker\n        // spawned it in project_cwd); harness-hosted: bind_from_seed passes the seed's\n        // captured cwd. A revive carries the prior cwd forward (a re-bind must not wipe\n        // it), like home/adapter/controllable.\n        // [impl->REQ-HAZARD-BIND-CWD-UNSET]\n        rec.cwd = cwd\n            .map(str::to_string)\n            .or_else(|| prior.as_ref().and_then(|p| p.cwd.clone()));\n        // Capture the adapter's manifest-declared `[env] direction = \"read\"` vars from\n        // THIS bind's launch environment (REQ-DIGEST-PROFILE-ENV). Bind is the only\n        // point the profile env is present in-process (harness-hosted: `api listen`\n        // runs inside the harness under its ccs profile; spt-hosted: `api bind` runs in\n        // the broker-spawned harness wrapper that inherited the spawn env). The\n        // on-demand `[digest]` extractor runs LATER in the daemon context where that env\n        // is gone, so a profile-relocated transcript root (e.g. ccs's\n        // `CLAUDE_CONFIG_DIR`) must be persisted here to resolve then. Only DECLARED\n        // read-vars, only when set (an allowlist). A revive with none freshly present\n        // carries the prior capture forward (like cwd/adapter — a re-bind must not wipe\n        // a good value).\n        // [impl->REQ-DIGEST-PROFILE-ENV]\n        rec.read_env = adapter\n            .and_then(|a| spt_runtime::registry::resolve_option(&perch::adapters_dir(), a).ok())\n            .map(|(_, m)| spt_runtime::runtime::capture_read_env(&m, |k| std::env::var(k).ok()))\n            .filter(|captured| !captured.is_empty())\n            .or_else(|| {\n                prior\n                    .as_ref()\n                    .map(|p| p.read_env.clone())\n                    .filter(|c| !c.is_empty())\n            })\n            .unwrap_or_default();\n        // DEFECT A (REQ-HAZARD-BIND-REST-STATE-CARRY): a re-bind must NOT wipe the\n        // daemon-owned resting intent (D9-2, REQ-INST-3). `InfoJson::new` defaults\n        // `rest_state` to `None`, so a fresh record from a re-bind drops a\n        // `suspended`/`dormant` wake intent (flynn's tick11 `rest_state:active`\n        // vanish) — the SAME carry-forward discipline as cwd/controllable/read_env.\n        // Carry the dormant anchor with it so a carried `dormant` keeps its\n        // auto-suspend counting anchor (`dormant_since_ms` is present iff dormant).\n        // [impl->REQ-HAZARD-BIND-REST-STATE-CARRY]\n        rec.rest_state = prior.as_ref().and_then(|p| p.rest_state.clone());\n        rec.dormant_since_ms = prior.as_ref().and_then(|p| p.dormant_since_ms);\n        Ok(rec)\n    };\n    let rec = match info::establish_locked(&perch_path, build) {\n        Ok(inner) => inner?,\n        Err(io) => return Err(BindError::Io(io.to_string())),\n    };\n    std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\n        .map_err(|e| BindError::Io(e.to_string()))?;\n\n    // Session ledger (REQ-TERM-6): record this session as active so the digest\n    // can span a later `/clear`|`/compact` boundary. The boot row dedups against\n    // a same-session revive; a missing/unstamped session is not ledgered (the\n    // digest spans only when there is a session to enumerate). Best-effort — a\n    // ledger problem must never fail the bind.\n    if !session_id.is_empty() {\n        let _ = spt_store::sessions::append(\n            &perch_path,\n            &spt_store::sessions::SessionEntry {\n                ts: spt_store::timefmt::now_rfc3339(),\n                session_id: session_id.to_string(),\n                trigger: spt_store::sessions::SessionTrigger::Boot,\n                // Record the bind cwd (W3 `rec.cwd`, just written above) so a\n                // later picker Resume of this boot session restores its project\n                // dir (REQ-SESSION-RESUME-TEMPLATE).\n                cwd: rec.cwd.clone(),\n                // `append` stamps the persisted monotonic ordinal (REQ-DIGEST-CURSOR).\n                ordinal: None,\n                // Record the adapter this boot session runs under so a later\n                // Resume restores the same harness (D-2, REQ-SESSION-ADAPTER-RECORDED).\n                // [impl->REQ-SESSION-ADAPTER-RECORDED]\n                adapter: rec.adapter.clone(),\n            },\n        );\n        // New session/cwd → refresh this endpoint's project-index row (+ the\n        // shared cwd cache for the bind cwd) — ADR-0037 §3. Fire-and-forget.\n        // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n        spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n            id: id.to_string(),\n            cwd: rec.cwd.clone(),\n        });\n    }\n\n    // Keep an existing token across a revive; mint one for a fresh perch.\n    let token = auth::read_token(&perch_path).unwrap_or_else(auth::mint_token);\n    auth::write_token(&perch_path, &token).map_err(|e| BindError::Io(e.to_string()))?;\n    Ok(token)\n}\n…\n/// `api listen <id>` — consume the seed, bind the perch, then run the relay\n/// loop (heir to `$LIVE start`'s Monitor-bound poll loop). `--once` drains and\n/// exits (testability); otherwise blocks streaming messages to stdout.\n///\n/// When the manifest is **live-capable** (declares `[session.psyche_init]`), the\n/// loop also hosts the LiveAgent lifecycle over the M2b `spt-live` seams: sweep\n/// stale signoff on start (3.2), spawn the Psyche (T2), and run a pulse tick\n/// each cycle (drop ingest + echo fires — T3/T4/T5). The graceful signoff (T6)\n/// is driven by the shutdown path; M3's daemon re-hosts all of it.\n// [impl->REQ-START-2]\n// [impl->REQ-SEAM-PSYCHE]\n/// How often the listen-orphan watchdog polls `--parent-pid` liveness\n/// (REQ-HAZARD-LISTEN-ORPHAN). A headless-gateway orphan's failure is \"held ONLINE for\n/// minutes\", so a seconds-latency poll is a full fix (doyle W4 ruling 2: POLL baseline,\n/// one code path both platforms; OS-native immediacy — Win job object / Linux PDEATHSIG\n/// — is an OPTIONAL follow-up). The listener exits within one of these windows.\nconst PARENT_WATCH_POLL: std::time::Duration = std::time::Duration::from_secs(2);\n\n/// Process exit code when the listener self-exits because its `--parent-pid` host died\n/// (REQ-HAZARD-LISTEN-ORPHAN) — distinct from a clean `0` or a refusal, so a log/operator\n/// can tell an orphan-exit from a normal end.\nconst EXIT_PARENT_GONE: i32 = 3;\n\n/// One watchdog-tick decision (REQ-HAZARD-LISTEN-ORPHAN): the listener must exit once its\n/// `--parent-pid` anchor is no longer alive. Pure over [`proc::is_process_alive`] so the\n/// exit trigger is unit-testable without a real host fork.\n// [impl->REQ-HAZARD-LISTEN-ORPHAN]\nfn parent_is_gone(parent_pid: u32) -> bool {\n    !proc::is_process_alive(parent_pid)\n}\n\n/// The loud orphan-exit line — names this listener + the dead parent, so a false-ONLINE\n/// perch becomes a diagnosable exit, never a silent hang. Pure for the unit.\n// [impl->REQ-HAZARD-LISTEN-ORPHAN]\nfn listen_orphan_exit_line(id: &str, parent_pid: u32) -> String {\n    format!(\n        \"LISTEN_ORPHAN_EXIT:{id}: parent pid {parent_pid} is gone — exiting the orphaned \\\n         listener so the perch flips OFFLINE (its recorded pid dies) and dead-owner rebind \\\n         unblocks [REQ-HAZARD-LISTEN-ORPHAN]\"\n    )\n}\n\n/// Spawn the listen-orphan watchdog (REQ-HAZARD-LISTEN-ORPHAN). `--parent-pid` is the\n/// auth anchor, but with no liveness watch a host death orphans the listener forever: the\n/// perch stays falsely ONLINE, EVENTs stream to a dead stdout, and dead-owner rebind is\n/// BLOCKED (the recorded pid is the live orphan). This bounded poll exits the process LOUD\n/// within one [`PARENT_WATCH_POLL`] window on parent death, so the perch's recorded pid\n/// dies → liveness flips it OFFLINE → rebind unblocks. Persistent-relay branch ONLY (a\n/// `--once` shot exits immediately — no orphan window). flynn's spt-mobile-side job-object\n/// guard stays as independent defense-in-depth (doyle W4 ruling 2); this is the core fix.\n// [impl->REQ-HAZARD-LISTEN-ORPHAN]\nfn spawn_parent_watchdog(id: &str, parent_pid: u32) {\n    let id = id.to_string();\n    std::thread::spawn(move || loop {\n        std::thread::sleep(PARENT_WATCH_POLL);\n        if parent_is_gone(parent_pid) {\n            eprintln!(\"{}\", listen_orphan_exit_line(&id, parent_pid));\n            std::process::exit(EXIT_PARENT_GONE);\n        }\n    });\n}\n\npub fn cmd_listen(\n    id: &str,\n    parent_pid_override: Option<u32>,\n    once: bool,\n    adapter: Option<&str>,\n    manifest: Option<&spt_runtime::Manifest>,\n    subnet: Option<&str>,\n    session_id: Option<&str>,\n) -> i32 {\n    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.\n    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {\n        eprintln!(\"INVALID_ID:{id}: {e}\");\n        return EXIT_REFUSED;\n    }\n    let parent_pid = match parent_pid_override.or_else(proc::parent_pid) {\n        Some(p) => p,\n        None => {\n            eprintln!(\"NO_PARENT_PID: could not resolve the parent pid anchor\");\n            return EXIT_REFUSED;\n        }\n    };\n\n    // Bind-time adapter/profile resolution (ADR-0021 / REQ-START-5): an explicit\n    // `--adapter` is the override; omitted, resolve the owning adapter from the\n    // seed's parent pid (exe basename → host_binaries candidates → active-profile\n    // pointer / registered_at_ms). A zero-match is a friendly refusal naming the\n    // binary + the `--adapter` escape — never a silent bind without an adapter\n    // (which would drop the Psyche, the very /sptc:live bug class).\n    // [impl->REQ-START-5]\n    let adapters_dir = spt_store::perch::adapters_dir();\n    let adapter_option: Option<String> = match adapter {\n        Some(a) => Some(a.to_string()),\n        None => match spt_runtime::resolve::resolve_bind_adapter(&adapters_dir, parent_pid) {\n            Ok(opt) => Some(opt),\n            Err(e) => {\n                eprintln!(\"ADAPTER_UNRESOLVED:{id}: {e}\");\n                return EXIT_REFUSED;\n            }\n        },\n    };\n    // The effective manifest: an explicit one (resolved in `run` from\n    // `--adapter`/`--manifest`), else the resolved adapter's registered manifest —\n    // the source of `live_capable` (psyche_init) + endpoint-native notif render.\n    let resolved_manifest: Option<spt_runtime::Manifest> = match manifest {\n        Some(m) => Some(m.clone()),\n        None => adapter_option.as_deref().and_then(|opt| {\n            spt_runtime::registry::resolve_option(&adapters_dir, opt)\n                .ok()\n                .map(|(_, m)| m)\n        }),\n    };\n    let manifest = resolved_manifest.as_ref();\n\n    let token = match bind_from_seed(id, parent_pid, subnet, adapter_option.as_deref()) {\n        Ok(t) => t,\n        // Late go-live / post-daemon-restart (F-034 leg c): the ephemeral seed\n        // is gone and nothing re-fires it until the NEXT SessionStart, so an\n        // explicit --session-id binds directly. NO_SEED only — any OTHER\n        // refusal keeps its own diagnostic (the sid fallback must never paper\n        // over a stale anchor, a live conflict, or a HOME refusal). Loud\n        // distinct marker so a field log shows which path bound.\n        // [impl->REQ-LISTEN-SESSION-ID-FALLBACK]\n        Err(BindError::NoSeed(_)) if session_id.is_some_and(|s| !s.is_empty()) => {\n            let sid = session_id.expect(\"guarded by the match arm\");\n            match bind_from_session_id(id, sid, parent_pid, subnet, adapter_option.as_deref()) {\n                Ok(t) => {\n                    eprintln!(\"SID_BIND:{id}: no live seed — bound from --session-id\");\n                    t\n                }\n                Err(e) => {\n                    report_bind_error(&e);\n                    return EXIT_REFUSED;\n                }\n            }\n        }\n        Err(e) => {\n            report_bind_error(&e);\n            return EXIT_REFUSED;\n        }\n    };\n    eprintln!(\"BOUND:{id} token={token}\");\n\n    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n    let relay = match spt_daemon::relay::Relay::bind(id, &perch_path) {\n        Ok(r) => r,\n        Err(e) => {\n            eprintln!(\"LISTEN_FAIL:{id}: {e}\");\n            return EXIT_REFUSED;\n        }\n    };\n\n    // Live-agent FIRST-HOST handoff to the daemon (M11-W0.2, REQ-DAEMON-1): the\n    // Psyche/pulse lifecycle is no longer hosted in THIS process — it lives in the\n    // brain (`spt_daemon::livehost`). For a live-capable endpoint (manifest\n    // declares `[session.psyche_init]`) we mark the perch `status=online` at\n    // establish; the brain's reconcile then hosts the Psyche within one tick (the\n    // shellwake CLI-flips-status / brain-reconciles pattern). `status` is\n    // daemon-authoritative (CONTEXT:194). A new `listen` against a not-yet-restarted\n    // OLD brain leaves the perch online-but-unhosted; that self-heals at the brain's\n    // next BOOT reconcile (livehost reconciles at boot, not only on tick) — the\n    // agent + the message relay below are live throughout regardless.\n    // [impl->REQ-DAEMON-1]\n    let live_capable = manifest.is_some_and(|m| m.session.psyche_init.is_some());\n    if live_capable {\n        if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {\n            eprintln!(\"LIVE_ONLINE_FAIL:{id}: {e}\");\n        }\n    }\n\n    eprintln!(\"READY:{id}\");\n\n    // New-session-start is a reported resurface boundary (ADR-0007/D8b):\n    // undismissed notifs surface into the spool before the backlog drain, so\n    // they ride this very first drain — and render endpoint-native when the\n    // manifest declares [session.notif]. Best-effort — never fails the listen.\n    super::reporting::resurface_notifs_with(id, manifest);\n\n    // Deliver one message to the live agent and record it as an spt context\n    // injection for the two-origin digest (REQ-TERM-7) — `emit` is the\n    // agent-facing chokepoint, so every delivered message becomes a glanceable\n    // `owl_message` context entry. The recorded form is the whole composed\n    // `<EVENT>` (what the agent sees), not the structural pair. Best-effort;\n    // never disturbs delivery.\n    let mut deliver = |from: &str, body: &str| {\n        emit(from, body);\n        let event = spt_msg::emit::render_event_whole(from, body);\n        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, &event);\n    };\n\n    if once {\n        // Single-shot (testability): drain the offline backlog (incl the just-\n        // resurfaced notifs) then forward one live delivery, and exit.\n        relay.drain_backlog(&mut deliver);\n        relay.pump_once(&mut deliver);\n        return 0;\n    }\n    // Listen-orphan watchdog (REQ-HAZARD-LISTEN-ORPHAN): watch the `--parent-pid` host\n    // and exit LOUD if it dies, so this listener never orphans the perch falsely ONLINE\n    // (EVENTs to a dead stdout, dead-owner rebind blocked). Persistent branch only.\n    // [impl->REQ-HAZARD-LISTEN-ORPHAN]\n    spawn_parent_watchdog(id, parent_pid);\n\n    // The pure relay loop (M11-W0.2): `Relay::run` drains the backlog first\n    // (preserving the resurface→drain ordering) then streams live deliveries to\n    // stdout until the process is killed. No lifecycle state lives in this process\n    // anymore — the Psyche/pulse runs in the brain. The stop flag is never raised;\n    // the harness kills the process to end the session (or the watchdog exits it on\n    // parent death).\n    let stop = std::sync::atomic::AtomicBool::new(false);\n    relay.run(&stop, &mut deliver);\n    0\n}\n\n/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the\n/// live perch on first contact, or flips an existing skeleton, keyed to the\n/// session id the binary reports. Auth is intrinsic: an existing *live* perch\n/// under a different session id is refused (you can only bind your own).\n// [impl->REQ-SEAM-POSTSPAWN]\npub fn cmd_bind(\n    id: &str,\n    set_session_id: Option<String>,\n    proof_session: Option<String>,\n    adapter: Option<&str>,\n    subnet: Option<&str>,\n    endpoint_type: &str,\n    manifest: Option<&spt_runtime::Manifest>,\n) -> i32 {\n    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.\n    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {\n        eprintln!(\"INVALID_ID:{id}: {e}\");\n        return EXIT_REFUSED;\n    }\n    let session_id = set_session_id\n        .or(proof_session)\n        .or_else(|| std::env::var(\"OWL_SESSION_ID\").ok())\n        .filter(|s| !s.is_empty());\n    let Some(session_id) = session_id else {\n        eprintln!(\"USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)\");\n        return EXIT_REFUSED;\n    };\n    // The session's own anchor: the process that fired this hook (the session).\n    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);\n    // spt-hosted: the daemon spawned a live_agent into a broker-owned PTY → it IS\n    // broker-PTY-controllable (REQ-PICKER-1). A non-live bind (a gateway/shell — not\n    // a PTY-attachable agent) stamps None; the picker's type-gated derivation never\n    // amber-paints it regardless.\n    let controllable = if endpoint_type == \"live_agent\" {\n        Some(true)\n    } else {\n        None\n    };\n    // W3 (REQ-HAZARD-BIND-CWD-UNSET): the broker spawned this spt-hosted bind\n    // subprocess in the endpoint's PROJECT cwd (REQ-HOST-RUN-2), so our own\n    // current_dir IS the project dir — record it on the perch.\n    let cwd = std::env::current_dir()\n        .ok()\n        .map(|p| p.to_string_lossy().into_owned());\n    match establish_perch(\n        id,\n        &session_id,\n        parent_pid,\n        adapter,\n        subnet,\n        endpoint_type,\n        controllable,\n        cwd.as_deref(),\n    ) {\n        Ok(token) => {\n            eprintln!(\"BOUND:{id} token={token}\");\n            // Live-agent FIRST-HOST handoff for the spt-hosted (bind, no listen)\n            // topology (M11-W0.3, REQ-DAEMON-1) — the mirror of W0.2's cmd_listen\n            // mark. A live_agent endpoint whose manifest declares\n            // `[session.psyche_init]` is marked status=online at establish; the\n            // brain's livehost reconcile then hosts the Psyche (the SAME single host\n            // path — bind never spawns/hosts/calls livehost). Closes the\n            // bind-no-Psyche gap: the Psyche spawn was previously cmd_listen-only, so\n            // a bind-only spt-hosted live agent never got one (spt-hosted uses direct\n            // PTY, no relay — CONTEXT:39). Gated on state==live_agent ∧ psyche_init so\n            // the establish-online signal == the reconcile host predicate (no\n            // online-but-unhostable perch). status is daemon-authoritative (CONTEXT:194).\n            // [impl->REQ-DAEMON-1]\n            let live_capable = endpoint_type == \"live_agent\"\n                && manifest.is_some_and(|m| m.session.psyche_init.is_some());\n            if live_capable {\n                let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n                if let Err(e) = info::set_status(&perch_path, spt_store::liveness::STATUS_ONLINE) {\n                    eprintln!(\"LIVE_ONLINE_FAIL:{id}: {e}\");\n                }\n            }\n            // A post-spawn bind is a new-session-start — a reported resurface\n            // boundary (ADR-0007/D8b). Best-effort.\n            super::reporting::resurface_notifs(id);\n            0\n        }\n        Err(e) => {\n            report_bind_error(&e);\n            EXIT_REFUSED\n        }\n    }\n}\n\nfn report_bind_error(e: &BindError) {\n    match e {\n        BindError::NoSeed(p) => eprintln!(\"NO_SEED:{p} (no seed for this parent pid)\"),\n        BindError::StaleSeed(p) => eprintln!(\"STALE_SEED:{p} (anchor process is dead)\"),\n        BindError::EmptySession => eprintln!(\"EMPTY_SESSION: seed carried no session id\"),\n        BindError::Conflict { id, held } => {\n            eprintln!(\"CONFLICT:{id} is live under session {held}\")\n        }\n        BindError::Home(m) => eprintln!(\"HOME_REFUSED: {m}\"),\n        BindError::PsycheCustodySquat { id, psyche } => eprintln!(\n            \"PSYCHE_CUSTODY_SQUAT:{id} refused — the requested session id is psyche \\\n             {psyche}'s own custody sid (a real perch must never pin to a psyche sid)\"\n        ),\n        BindError::Io(m) => eprintln!(\"BIND_IO: {m}\"),\n    }\n}\n\n/// Emit one delivery to the listener stream: compose the structural\n/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass\n/// through; oversized lines chunk into EVENT-PART) and print + flush. One\n/// delivery per emit.\n// [impl->REQ-MSG-4]\n// [impl->REQ-MSG-ENVELOPE]\nfn emit(from: &str, body: &str) {\n    use std::io::Write;\n    for line in spt_msg::emit::render_event_lines(from, body) {\n        println!(\"{line}\");\n    }\n    let _ = std::io::stdout().flush();\n}\n\n/// Coarse creation timestamp (epoch seconds as a string), matching the\n/// `info.json` `started` producer-format convention.\nfn now_stamp() -> String {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_secs().to_string())\n        .unwrap_or_else(|_| \"0\".to_string())\n}","startLine":92,"lineNumbers":[92,null,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,null,252,null,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,null,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\api\\startup.rs"}}},"isError":false,"timestamp":1784197455165}}
{"type":"message","id":"9fcd7096","parentId":"b123eb42","timestamp":"2026-07-16T10:24:15.166Z","message":{"role":"toolResult","toolCallId":"call_uHpMBuvDQmU93psCmnIvWSZO|fc_017bff223290b7ba016a58b14f1d30819a8deff5a4452cde9c","toolName":"read","content":[{"type":"text","text":"//! `PollListener` — a loopback TCP accept listener with registry lifecycle.\n//!\n//! Clean-room re-expression of the sister `common/listener.rs` against the\n//! spt-core `spt-store` API (perch resolver + registry). The socket plumbing\n//! and the cross-platform `poll_ready` syscall wrapper are mechanical and match\n//! the sister byte-for-byte in behavior; the registry/info.json lifecycle is\n//! re-pointed at `spt_store`.\n//!\n//! Lifecycle: [`PollListener::bind`] grabs an ephemeral `127.0.0.1:0` port and\n//! writes the perch's address into the registry under the owning pid. On drop\n//! (or [`PollListener::close`]) the registry entry is removed so no stale\n//! address survives a crashed listener — the recycled-pid guard\n//! ([`spt_store::registry::resolve_address`]) is the second line of defense.\n//!\n//! **Interim model (M1):** one standalone listener per ready agent. M3's\n//! `spt-daemon` consolidates all listeners into one supervisor; the surface is\n//! kept thin so that consolidation is a move, not a rewrite.\n// [impl->REQ-MSG-1]\n// [impl->REQ-MSG-3]\n\nuse spt_store::info::{self, PidValue};\nuse spt_store::perch::{self, ParentHint};\nuse spt_store::registry;\nuse std::io::ErrorKind;\nuse std::net::{SocketAddr, TcpListener};\nuse std::path::PathBuf;\nuse std::time::Duration;\n\nuse crate::wire;\n\n/// A loopback TCP listener that owns its registry registration.\npub struct PollListener {\n    listener: TcpListener,\n    id: String,\n    owlery: PathBuf,\n    perch_path: PathBuf,\n    closed: bool,\n    /// Poll timeout for the blocking accept path; `None` => default 500 ms.\n    timeout_ms: Option<i32>,\n}\n\n…\nimpl PollListener {\n…\n    pub fn bind_blocking(id: &str, timeout: Duration) -> Result<Self, String> {\n…\n        Self::bind_inner(id, Some(ms))\n    }\n\n    fn bind_inner(id: &str, timeout_ms: Option<i32>) -> Result<Self, String> {\n        let owlery = perch::owlery_dir();\n        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n\n        let listener = TcpListener::bind(\"127.0.0.1:0\")\n            .map_err(|e| format!(\"Failed to bind TCP listener: {}\", e))?;\n        // Non-blocking listeners poll explicitly; blocking listeners keep the\n        // OS default and gate accept behind poll_ready with a timeout.\n        if timeout_ms.is_none() {\n            listener\n                .set_nonblocking(true)\n                .map_err(|e| format!(\"Failed to set non-blocking: {}\", e))?;\n        }\n        let addr = listener\n            .local_addr()\n            .map_err(|e| format!(\"Failed to get local address: {}\", e))?;\n        registry::register_address(id, &addr, &owlery)\n            .map_err(|e| format!(\"Failed to register address: {}\", e))?;\n\n        Ok(Self {\n            listener,\n            id: id.to_string(),\n            owlery,\n            perch_path,\n            closed: false,\n            timeout_ms,\n        })\n    }\n\n    /// The bound local address (`127.0.0.1:<ephemeral>`).\n    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {\n        self.listener.local_addr()\n    }\n\n    /// Try to accept one pending connection and read its framed message.\n    /// Returns `Some((from, body))` if one was waiting, `None` otherwise (no\n    /// panic). The structural pair — the delivery edge composes the `<EVENT>`.\n    pub fn check_message(&self) -> Option<(String, String)> {\n        match self.listener.accept() {\n            Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),\n            Err(ref e) if e.kind() == ErrorKind::WouldBlock => None,\n            Err(_) => None,\n        }\n    }\n\n    /// Wait (up to the configured timeout, default 500 ms) for a connection via\n    /// the OS poll syscall, then accept+read one framed message as `(from, body)`.\n    pub fn check_message_blocking(&self) -> Option<(String, String)> {\n        let timeout_ms = self.timeout_ms.unwrap_or(500);\n        if poll_ready(&self.listener, timeout_ms) {\n            match self.listener.accept() {\n                Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),\n                Err(_) => None,\n            }\n        } else {\n            None\n        }\n    }\n\n    /// Unregister this perch's address from the registry. Idempotent.\n    pub fn close(&mut self) {\n        let _ = registry::unregister_address(&self.id, &self.owlery);\n        self.closed = true;\n    }\n\n    /// Unregister and stamp `info.json` with the `BUSY` pid sentinel — the\n    /// \"listener alive but between polls\" state a sender may observe. Used when\n    /// the perch persists but the socket is torn down mid-cycle.\n    pub fn close_busy(&mut self) {\n        let _ = registry::unregister_address(&self.id, &self.owlery);\n        write_busy(&self.perch_path);\n        self.closed = true;\n    }\n}\n\nimpl Drop for PollListener {\n    fn drop(&mut self) {\n        if !self.closed {\n            // Default teardown: drop the registry address so no stale entry\n            // outlives the socket. info.json/spool are left untouched\n            // (soft-cleanup semantics live in the ready lifecycle, T3).\n            let _ = registry::unregister_address(&self.id, &self.owlery);\n        }\n    }\n}\n\n/// Rewrite `<perch>/info.json` with the `BUSY` pid sentinel, preserving every\n/// other field. Best-effort: a missing/unparseable info.json is a no-op.\npub(crate) fn write_busy(perch_path: &std::path::Path) {\n    if let Some(mut info) = info::read_info(perch_path) {\n        info.pid = PidValue::Busy(\"BUSY\".to_string());\n        let _ = info::write_info(perch_path, &info);\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::net::TcpStream;\n    use std::time::Instant;\n\n    /// Unique perch id per invocation (pid + nanos) so concurrent tests sharing\n    /// the process-global SPT_HOME registry never collide.\n    fn unique_id(prefix: &str) -> String {\n        let nanos = std::time::SystemTime::now()\n            .duration_since(std::time::UNIX_EPOCH)\n            .map(|d| d.as_nanos())\n            .unwrap_or(0);\n        format!(\"{}-{}-{}\", prefix, std::process::id(), nanos)\n    }\n\n    // [unit->REQ-MSG-1] a non-blocking listener binds a loopback port, registers\n    // its address, and a framed envelope round-trips client -> listener.\n    #[test]\n    fn bind_registers_and_round_trips() {\n        crate::test_home::isolate();\n        let id = unique_id(\"test-bind\");\n        let listener = PollListener::bind(&id).unwrap();\n        let addr = listener.local_addr().unwrap();\n\n        // Registry holds the bound address.\n        let owlery = perch::owlery_dir();\n        assert_eq!(\n            registry::lookup_address(&id, &owlery),\n            Some(addr.to_string())\n        );\n\n        // Deliver one framed message.\n        let mut client = TcpStream::connect(addr).unwrap();\n        wire::write_message(&mut client, \"sender\", \"ping\").unwrap();\n        drop(client);\n\n        // Non-blocking accept may briefly WouldBlock before the connection lands.\n        let mut got = None;\n        for _ in 0..200 {\n            if let Some(m) = listener.check_message() {\n                got = Some(m);\n                break;\n            }\n            std::thread::sleep(Duration::from_millis(5));\n        }\n        assert_eq!(got, Some((\"sender\".to_string(), \"ping\".to_string())));\n    }\n\n    // [unit->REQ-MSG-1] dropping a listener removes its registry address (no\n    // stale entry survives a dead listener).\n    #[test]\n    fn drop_unregisters_address() {\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"//! `PollListener` — a loopback TCP accept listener with registry lifecycle.\n//!\n//! Clean-room re-expression of the sister `common/listener.rs` against the\n//! spt-core `spt-store` API (perch resolver + registry). The socket plumbing\n//! and the cross-platform `poll_ready` syscall wrapper are mechanical and match\n//! the sister byte-for-byte in behavior; the registry/info.json lifecycle is\n//! re-pointed at `spt_store`.\n//!\n//! Lifecycle: [`PollListener::bind`] grabs an ephemeral `127.0.0.1:0` port and\n//! writes the perch's address into the registry under the owning pid. On drop\n//! (or [`PollListener::close`]) the registry entry is removed so no stale\n//! address survives a crashed listener — the recycled-pid guard\n//! ([`spt_store::registry::resolve_address`]) is the second line of defense.\n//!\n//! **Interim model (M1):** one standalone listener per ready agent. M3's\n//! `spt-daemon` consolidates all listeners into one supervisor; the surface is\n//! kept thin so that consolidation is a move, not a rewrite.\n// [impl->REQ-MSG-1]\n// [impl->REQ-MSG-3]\n\nuse spt_store::info::{self, PidValue};\nuse spt_store::perch::{self, ParentHint};\nuse spt_store::registry;\nuse std::io::ErrorKind;\nuse std::net::{SocketAddr, TcpListener};\nuse std::path::PathBuf;\nuse std::time::Duration;\n\nuse crate::wire;\n\n/// A loopback TCP listener that owns its registry registration.\npub struct PollListener {\n    listener: TcpListener,\n    id: String,\n    owlery: PathBuf,\n    perch_path: PathBuf,\n    closed: bool,\n    /// Poll timeout for the blocking accept path; `None` => default 500 ms.\n    timeout_ms: Option<i32>,\n}\n\n…\nimpl PollListener {\n…\n    pub fn bind_blocking(id: &str, timeout: Duration) -> Result<Self, String> {\n…\n        Self::bind_inner(id, Some(ms))\n    }\n\n    fn bind_inner(id: &str, timeout_ms: Option<i32>) -> Result<Self, String> {\n        let owlery = perch::owlery_dir();\n        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n\n        let listener = TcpListener::bind(\"127.0.0.1:0\")\n            .map_err(|e| format!(\"Failed to bind TCP listener: {}\", e))?;\n        // Non-blocking listeners poll explicitly; blocking listeners keep the\n        // OS default and gate accept behind poll_ready with a timeout.\n        if timeout_ms.is_none() {\n            listener\n                .set_nonblocking(true)\n                .map_err(|e| format!(\"Failed to set non-blocking: {}\", e))?;\n        }\n        let addr = listener\n            .local_addr()\n            .map_err(|e| format!(\"Failed to get local address: {}\", e))?;\n        registry::register_address(id, &addr, &owlery)\n            .map_err(|e| format!(\"Failed to register address: {}\", e))?;\n\n        Ok(Self {\n            listener,\n            id: id.to_string(),\n            owlery,\n            perch_path,\n            closed: false,\n            timeout_ms,\n        })\n    }\n\n    /// The bound local address (`127.0.0.1:<ephemeral>`).\n    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {\n        self.listener.local_addr()\n    }\n\n    /// Try to accept one pending connection and read its framed message.\n    /// Returns `Some((from, body))` if one was waiting, `None` otherwise (no\n    /// panic). The structural pair — the delivery edge composes the `<EVENT>`.\n    pub fn check_message(&self) -> Option<(String, String)> {\n        match self.listener.accept() {\n            Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),\n            Err(ref e) if e.kind() == ErrorKind::WouldBlock => None,\n            Err(_) => None,\n        }\n    }\n\n    /// Wait (up to the configured timeout, default 500 ms) for a connection via\n    /// the OS poll syscall, then accept+read one framed message as `(from, body)`.\n    pub fn check_message_blocking(&self) -> Option<(String, String)> {\n        let timeout_ms = self.timeout_ms.unwrap_or(500);\n        if poll_ready(&self.listener, timeout_ms) {\n            match self.listener.accept() {\n                Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),\n                Err(_) => None,\n            }\n        } else {\n            None\n        }\n    }\n\n    /// Unregister this perch's address from the registry. Idempotent.\n    pub fn close(&mut self) {\n        let _ = registry::unregister_address(&self.id, &self.owlery);\n        self.closed = true;\n    }\n\n    /// Unregister and stamp `info.json` with the `BUSY` pid sentinel — the\n    /// \"listener alive but between polls\" state a sender may observe. Used when\n    /// the perch persists but the socket is torn down mid-cycle.\n    pub fn close_busy(&mut self) {\n        let _ = registry::unregister_address(&self.id, &self.owlery);\n        write_busy(&self.perch_path);\n        self.closed = true;\n    }\n}\n\nimpl Drop for PollListener {\n    fn drop(&mut self) {\n        if !self.closed {\n            // Default teardown: drop the registry address so no stale entry\n            // outlives the socket. info.json/spool are left untouched\n            // (soft-cleanup semantics live in the ready lifecycle, T3).\n            let _ = registry::unregister_address(&self.id, &self.owlery);\n        }\n    }\n}\n\n/// Rewrite `<perch>/info.json` with the `BUSY` pid sentinel, preserving every\n/// other field. Best-effort: a missing/unparseable info.json is a no-op.\npub(crate) fn write_busy(perch_path: &std::path::Path) {\n    if let Some(mut info) = info::read_info(perch_path) {\n        info.pid = PidValue::Busy(\"BUSY\".to_string());\n        let _ = info::write_info(perch_path, &info);\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use std::net::TcpStream;\n    use std::time::Instant;\n\n    /// Unique perch id per invocation (pid + nanos) so concurrent tests sharing\n    /// the process-global SPT_HOME registry never collide.\n    fn unique_id(prefix: &str) -> String {\n        let nanos = std::time::SystemTime::now()\n            .duration_since(std::time::UNIX_EPOCH)\n            .map(|d| d.as_nanos())\n            .unwrap_or(0);\n        format!(\"{}-{}-{}\", prefix, std::process::id(), nanos)\n    }\n\n    // [unit->REQ-MSG-1] a non-blocking listener binds a loopback port, registers\n    // its address, and a framed envelope round-trips client -> listener.\n    #[test]\n    fn bind_registers_and_round_trips() {\n        crate::test_home::isolate();\n        let id = unique_id(\"test-bind\");\n        let listener = PollListener::bind(&id).unwrap();\n        let addr = listener.local_addr().unwrap();\n\n        // Registry holds the bound address.\n        let owlery = perch::owlery_dir();\n        assert_eq!(\n            registry::lookup_address(&id, &owlery),\n            Some(addr.to_string())\n        );\n\n        // Deliver one framed message.\n        let mut client = TcpStream::connect(addr).unwrap();\n        wire::write_message(&mut client, \"sender\", \"ping\").unwrap();\n        drop(client);\n\n        // Non-blocking accept may briefly WouldBlock before the connection lands.\n        let mut got = None;\n        for _ in 0..200 {\n            if let Some(m) = listener.check_message() {\n                got = Some(m);\n                break;\n            }\n            std::thread::sleep(Duration::from_millis(5));\n        }\n        assert_eq!(got, Some((\"sender\".to_string(), \"ping\".to_string())));\n    }\n\n    // [unit->REQ-MSG-1] dropping a listener removes its registry address (no\n    // stale entry survives a dead listener).\n    #[test]\n    fn drop_unregisters_address() {\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,null,79,null,88,null,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,null,249,null,282]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-msg\\src\\listener.rs"}}},"isError":false,"timestamp":1784197455165}}
{"type":"message","id":"9dfc3580","parentId":"9fcd7096","timestamp":"2026-07-16T10:24:15.176Z","message":{"role":"toolResult","toolCallId":"call_bA4fkq3A6VSXw4ESGMrAOiFc|fc_017bff223290b7ba016a58b14f1d64819a9d26473edf5bbbc9","toolName":"read","content":[{"type":"text","text":"use serde::{Deserialize, Serialize};\nuse std::path::Path;\n\nuse crate::atomic::atomic_write_string_durable;\n\n/// Pid field that is either a real numeric pid or the `\"BUSY\"` sentinel a\n/// listener writes while it holds the perch. Untagged: serializes as a bare\n/// integer or a bare string. Copied verbatim (sister `PidValue`).\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(untagged)]\npub enum PidValue {\n    Numeric(u32),\n    Busy(String),\n}\n\n/// The current psyche-host-failure state of a daemon-hosted live perch\n/// (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE). The brain's per-event psyche-turn\n/// budget (`note_turn_outcome`, F-030) stamps this on a Self perch when a\n/// bounded Psyche turn fails, so the failure is **harness-reachable** — a\n/// harness (and a human via `spt endpoint list` / `spt whoami`) reads PERCH\n/// STATE, never the brain's invisible stderr. It is a CURRENT-STATE stamp\n/// (overwritten each retry, `attempts` incremented; cleared to `None` on a\n/// later successful turn), NOT an append log. Distinct from `status`:\n/// `status` (online/offline) stays daemon-authoritative liveness (KH 2.5) and\n/// is **never** touched by this — an online live agent with no Psyche reads\n/// `status=online` AND a `psyche_host_error`, exposing the boot-race that was\n/// previously silent.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct PsycheHostError {\n    /// The Psyche-turn error string from the most recent failed turn attempt.\n    pub reason: String,\n    /// RFC3339-UTC stamp of the most recent failed attempt (the same clock\n    /// [`crate::timefmt::now_rfc3339`] stamps every other info.json string ts).\n    pub ts: String,\n    /// How many consecutive host attempts have failed (incremented each retry;\n    /// `1` on the first failure).\n    pub attempts: u32,\n}\n\n/// On-disk shape of a perch's `info.json`. Field order is locked by declaration\n/// (serde_json `preserve_order`). Optional fields are omitted when `None`.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InfoJson {\n    /// The perch / agent id.\n    pub id: String,\n    /// Creation timestamp (opaque string; producer's format).\n    pub started: String,\n    /// Owning process pid, or the `\"BUSY\"` sentinel.\n    pub pid: PidValue,\n    /// Harness session id bound to this perch.\n    pub session_id: String,\n    /// Endpoint type tag (`spt_proto::endpoint::EndpointType::as_tag`).\n    pub state: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub parent_pid: Option<u32>,\n    /// Working directory the perch was created in (forward-slash normalized).\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub cwd: Option<String>,\n    /// Daemon-managed liveness status (`online`/`offline`) for **daemon-hosted**\n    /// perches (Psyche, spt-hosted Self). Present ⇒ the daemon owns this perch's\n    /// liveness and it is read authoritatively, **never** `is_process_alive(pid)`\n    /// (KNOWN-HAZARDS 2.5). Absent ⇒ an external/interim perch whose liveness is\n    /// the per-pid probe. The single liveness resolver\n    /// ([`crate::liveness::is_perch_alive`]) is the only consumer.\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub status: Option<String>,\n    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this\n    /// endpoint's activity (pulse tick / input / bind). The recency signal the\n    /// consent gate resolves \"most-recently-active live session\" against\n    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never\n    /// stamped (an interim perch, or one with no activity yet); a resolver treats\n    /// that as least-recent.\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub last_active_ms: Option<u64>,\n    /// The node currently remote-driving this endpoint's session (hex node id),\n    /// stamped by the daemon when it serves a net-attached surface and cleared\n    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an\n    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`\n    /// so a pre-D5c record (no field) still parses.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub driven_by: Option<String>,\n    /// Whether ANY operator — local OR remote — currently drives this endpoint's\n    /// session (#3, REQ-GOSSIP-CONTROLLED-ANY). The BROKER is the single writer,\n    /// stamped alongside `driven_by` on every controller add/clear. DISTINCT from\n    /// `driven_by`, which is `Some` ONLY for a REMOTE controller (a local-only\n    /// controller must NOT latch `driven_by` — KH 7.15 /\n    /// `REQ-HAZARD-DRIVEN-BY-SELFHEAL`): `driven_by` answers \"is a remote node\n    /// driving me\", `controlled` answers \"is ANYONE driving me\", so a\n    /// locally-controlled endpoint still gossips as controlled to remote viewers.\n    /// `default false` so a pre-#3 record parses.\n    #[serde(default)]\n    pub controlled: bool,\n    /// The number of read-only viewers currently attached to this endpoint's\n    /// session under the W2.5 controller/viewer model (REQ-RCVIEW-1). The BROKER\n    /// is the single writer (it owns the viewers map), stamped on every viewer\n    /// add/remove/evict; the picker renders `controlled by <driven_by> (+N\n    /// viewing)`. Absent / `0` ⇒ no viewers (or a pre-W2.5 record). Same\n    /// best-effort/self-heal caveat as `driven_by`. `default` so older records\n    /// still parse.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub viewer_count: Option<u32>,\n    /// The daemon-owned resting state (D9-2, REQ-INST-3): `\"active\"` /\n    /// `\"dormant\"` / `\"suspended\"` — the explicit instance state machine's\n    /// durable record (`spt_daemon::resting` owns the transitions; this field\n    /// is only ever written through [`set_rest_state`]). Distinct from\n    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);\n    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a\n    /// pre-D9-2 / interim record (readers fall back to liveness-derived\n    /// status). `default` so older records still parse.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub rest_state: Option<String>,\n    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in\n    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from\n    /// dormancy onset, never from last activity). Present iff `rest_state`\n    /// is `\"dormant\"`; cleared on every other transition.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub dormant_since_ms: Option<u64>,\n    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of\n    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit\n    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint\n    /// even when the node enables it.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub auto_suspend_after_ms: Option<u64>,\n    /// The endpoint-authored resource-advertisement blurb (D9-4,\n    /// REQ-INST-14): the agent's own \"what I can serve\" line, written via\n    /// `spt resources set` ([`set_resources`]) and advertised into every\n    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level\n    /// `daemon.json` seed (if any) advertises instead — config seeds the\n    /// default, the agent refines its own at runtime (CONTEXT §resource\n    /// advertisement, both-authored).\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub resources: Option<String>,\n    /// The endpoint's **immutable home subnet** (D9-5, REQ-INST-15 —\n    /// ADR-0010): assigned at creation ([`crate::home::assign_home`]) or by\n    /// first-join adoption ([`crate::home::adopt_for_unset`]), then never\n    /// changed — **no setter exists**; `spt fork` is the cross-subnet move.\n    /// Absent ⇒ local-only (unpaired at creation, awaiting first join) or a\n    /// pre-D9-5 record. Creation seams must **carry this forward** on a\n    /// re-bind (a revive rewrites `info.json`; home survives by contract).\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub home_subnet: Option<String>,\n    /// The adapter chosen at creation (D9-5, REQ-INST-15 — the\n    /// adapter-at-creation rule): recorded when a creation/bind path knows\n    /// it, changed **only** via launch/resume-under-new (the bind paths are\n    /// the sole writers; no standalone setter). Absent ⇒ created by an\n    /// adapterless surface (`spt ready`) or a pre-D9-5 record.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub adapter: Option<String>,\n    /// The CURRENT psyche-host-failure state (v0.8.1,\n    /// REQ-HAZARD-LIVEHOST-BOOT-RACE): present ⇒ the brain's live-host\n    /// reconcile failed to spawn this perch's Psyche on its most recent\n    /// attempt (the boot-race / missing-psyche-binary failure that was\n    /// otherwise silent on the brain's stderr). Cleared (`None`) on a later\n    /// successful host, so a recovered live agent shows clean. `default` so\n    /// every pre-v0.8.1 record still parses (N-1-safe, additive). Set/cleared\n    /// only through [`set_psyche_host_error`].\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub psyche_host_error: Option<PsycheHostError>,\n    /// The most recent HOST-LEVEL failure for this endpoint (A-2,\n    /// REQ-WAKE-RESUME-LEG): present ⇒ the daemon's reconcile could not host/resume\n    /// it on its most recent attempt (e.g. the wake-resume leg's recorded adapter is\n    /// not registered). **Report-only, NEVER a liveness input** — neither\n    /// [`crate::liveness`] nor the A-1 effective-state derivation\n    /// (`advertised_status`) reads it, so a `host_error` beside `status=online`\n    /// still derives Active (the boot-race lesson: read-side truth stays\n    /// independent of a failure REPORT). Distinct from\n    /// [`psyche_host_error`](Self::psyche_host_error) (psyche-turn scoped, wrong\n    /// semantics for a host-level refusal). Cleared (`None`) on a successful\n    /// host/bind. `default` so every pre-A-2 record still parses (additive,\n    /// N-1-safe). Set/cleared only through [`set_host_error`].\n    // [impl->REQ-WAKE-RESUME-LEG]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub host_error: Option<String>,\n    /// Whether this endpoint is **broker-PTY-controllable** (REQ-PICKER-1): a\n    /// spt-hosted session the daemon spawned into a broker-owned PTY is `Some(true)`\n    /// (attachable/drivable); a harness-hosted LiveAgent (bound via `api listen` —\n    /// the harness owns the process, no broker PTY) is `Some(false)`. Stamped at the\n    /// establish seam (carry-forward on revive). `None` ⇒ a pre-v0.10.0 record, an\n    /// adapterless `ready`, or a non-live bind — the picker's status derivation\n    /// type-gates so only a LiveAgent ever renders the amber \"harness-only\" state.\n    /// `default` so every older record parses (N-1-safe, additive).\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub controllable: Option<bool>,\n    /// Whether this session's PTY input-writer thread is currently DROPPING input\n    /// because its bounded FIFO saturated — a genuinely wedged harness that has\n    /// stopped draining its stdin (v0.13.0 P0, REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).\n    /// The BROKER is the single writer (it owns the per-session input-writer):\n    /// `Some(true)` is stamped on a full-queue DROP, cleared (`None`) on the next\n    /// accepted enqueue once the writer drains a slot (heal-on-resume). The daemon\n    /// NEVER wedges on a stuck harness — it drops + surfaces this instead. Absent ⇒\n    /// no backpressure (or a pre-P0 record). `default` so every older record parses\n    /// (N-1-safe, additive); same best-effort/self-heal caveat as `viewer_count`.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub input_backpressure: Option<bool>,\n    /// The CURRENT translation-binary fault surface (F029 C-1,\n    /// REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH): present ⇒ this spt-hosted\n    /// session's idle-delivery translation binary FAULTED (a real binary death —\n    /// stdin-write fail / stdout disconnect — or exhaustion of the commit-miss\n    /// strike budget) and idle inbound is SPOOLING until a working binary. The\n    /// string is the daemon's fault reason (the same text `TRANSLATION_FAULT`\n    /// logs to the F-019 unread daemon-stderr channel — surfaced here so a live\n    /// session's degraded idle delivery is harness-reachable, the honesty rule\n    /// F-027's ENDPOINT_SPAWN_FAIL established). The BROKER is the single writer:\n    /// stamped on a real fault, cleared (`None`) on a healthy respawn (or the\n    /// adapter-apply restart). A mere commit-deadline MISS is NOT a fault and\n    /// never stamps this (the binary is preserved; REQ-TRANSLATE-COMMIT-MISS-\n    /// TOLERANCE). Absent ⇒ no fault (or a pre-C-1 record). `default` so every\n    /// older record parses (N-1-safe, additive); same best-effort/self-heal\n    /// caveat as `input_backpressure`.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub translation_fault: Option<String>,\n…\n}\n…\n    fn write_read_round_trip() {\n…\n        // parent_pid omitted (None) → absent from JSON.\n        let raw = std::fs::read_to_string(d.path().join(\"info.json\")).unwrap();\n        assert!(\n            !raw.contains(\"parent_pid\"),\n            \"None field must be omitted: {raw}\"\n        );\n        assert!(raw.contains(\"\\\"state\\\":\\\"live_agent\\\"\"));\n    }\n\n    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] BUSY sentinel: full parse keeps it;\n    // read_pid yields None (no usable pid).\n    #[test]\n    fn busy_sentinel_round_trips_and_read_pid_is_none() {\n        let d = perch();\n        let info = InfoJson {\n            id: \"doyle\".to_string(),\n            started: \"t\".to_string(),\n            pid: PidValue::Busy(\"BUSY\".to_string()),\n            session_id: \"sid\".to_string(),\n            state: \"ready_agent\".to_string(),\n            parent_pid: None,\n            cwd: None,\n            status: None,\n            last_active_ms: None,\n            driven_by: None,\n            controlled: false,\n            viewer_count: None,\n            rest_state: None,\n            dormant_since_ms: None,\n            auto_suspend_after_ms: None,\n            resources: None,\n            home_subnet: None,\n            adapter: None,\n            psyche_host_error: None,\n            host_error: None,\n            controllable: None,\n            input_backpressure: None,\n            translation_fault: None,\n            read_env: std::collections::BTreeMap::new(),\n            agent_id: None,\n            agent_type: None,\n        };\n        write_info(d.path(), &info).unwrap();\n        assert_eq!(\n            read_info(d.path()).unwrap().pid,\n            PidValue::Busy(\"BUSY\".to_string())\n        );\n        assert_eq!(read_pid(d.path()), None, \"BUSY must yield no numeric pid\");\n    }\n\n    // [unit->REQ-PICKER-1] N-1-safe: a pre-v0.10.0 record with no `controllable`\n    // key parses clean to `controllable == None` (additive, serde-default), and a\n    // stamped value round-trips.\n    #[test]\n    fn controllable_is_additive_and_round_trips() {\n        let d = perch();\n…\n    }"}],"details":{"displayContent":{"text":"use serde::{Deserialize, Serialize};\nuse std::path::Path;\n\nuse crate::atomic::atomic_write_string_durable;\n\n/// Pid field that is either a real numeric pid or the `\"BUSY\"` sentinel a\n/// listener writes while it holds the perch. Untagged: serializes as a bare\n/// integer or a bare string. Copied verbatim (sister `PidValue`).\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(untagged)]\npub enum PidValue {\n    Numeric(u32),\n    Busy(String),\n}\n\n/// The current psyche-host-failure state of a daemon-hosted live perch\n/// (v0.8.1, REQ-HAZARD-LIVEHOST-BOOT-RACE). The brain's per-event psyche-turn\n/// budget (`note_turn_outcome`, F-030) stamps this on a Self perch when a\n/// bounded Psyche turn fails, so the failure is **harness-reachable** — a\n/// harness (and a human via `spt endpoint list` / `spt whoami`) reads PERCH\n/// STATE, never the brain's invisible stderr. It is a CURRENT-STATE stamp\n/// (overwritten each retry, `attempts` incremented; cleared to `None` on a\n/// later successful turn), NOT an append log. Distinct from `status`:\n/// `status` (online/offline) stays daemon-authoritative liveness (KH 2.5) and\n/// is **never** touched by this — an online live agent with no Psyche reads\n/// `status=online` AND a `psyche_host_error`, exposing the boot-race that was\n/// previously silent.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct PsycheHostError {\n    /// The Psyche-turn error string from the most recent failed turn attempt.\n    pub reason: String,\n    /// RFC3339-UTC stamp of the most recent failed attempt (the same clock\n    /// [`crate::timefmt::now_rfc3339`] stamps every other info.json string ts).\n    pub ts: String,\n    /// How many consecutive host attempts have failed (incremented each retry;\n    /// `1` on the first failure).\n    pub attempts: u32,\n}\n\n/// On-disk shape of a perch's `info.json`. Field order is locked by declaration\n/// (serde_json `preserve_order`). Optional fields are omitted when `None`.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct InfoJson {\n    /// The perch / agent id.\n    pub id: String,\n    /// Creation timestamp (opaque string; producer's format).\n    pub started: String,\n    /// Owning process pid, or the `\"BUSY\"` sentinel.\n    pub pid: PidValue,\n    /// Harness session id bound to this perch.\n    pub session_id: String,\n    /// Endpoint type tag (`spt_proto::endpoint::EndpointType::as_tag`).\n    pub state: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub parent_pid: Option<u32>,\n    /// Working directory the perch was created in (forward-slash normalized).\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub cwd: Option<String>,\n    /// Daemon-managed liveness status (`online`/`offline`) for **daemon-hosted**\n    /// perches (Psyche, spt-hosted Self). Present ⇒ the daemon owns this perch's\n    /// liveness and it is read authoritatively, **never** `is_process_alive(pid)`\n    /// (KNOWN-HAZARDS 2.5). Absent ⇒ an external/interim perch whose liveness is\n    /// the per-pid probe. The single liveness resolver\n    /// ([`crate::liveness::is_perch_alive`]) is the only consumer.\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub status: Option<String>,\n    /// Last-active wall-clock stamp (epoch ms), refreshed by the daemon on this\n    /// endpoint's activity (pulse tick / input / bind). The recency signal the\n    /// consent gate resolves \"most-recently-active live session\" against\n    /// (REQ-UPD-4) — a v1 precursor to PresenceChannel dispatch. Absent ⇒ never\n    /// stamped (an interim perch, or one with no activity yet); a resolver treats\n    /// that as least-recent.\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub last_active_ms: Option<u64>,\n    /// The node currently remote-driving this endpoint's session (hex node id),\n    /// stamped by the daemon when it serves a net-attached surface and cleared\n    /// when the viewport ends (REQ-REACH-1: detection is a daemon fact, not an\n    /// agent guess). Absent ⇒ locally driven (or never net-attached). `default`\n    /// so a pre-D5c record (no field) still parses.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub driven_by: Option<String>,\n    /// Whether ANY operator — local OR remote — currently drives this endpoint's\n    /// session (#3, REQ-GOSSIP-CONTROLLED-ANY). The BROKER is the single writer,\n    /// stamped alongside `driven_by` on every controller add/clear. DISTINCT from\n    /// `driven_by`, which is `Some` ONLY for a REMOTE controller (a local-only\n    /// controller must NOT latch `driven_by` — KH 7.15 /\n    /// `REQ-HAZARD-DRIVEN-BY-SELFHEAL`): `driven_by` answers \"is a remote node\n    /// driving me\", `controlled` answers \"is ANYONE driving me\", so a\n    /// locally-controlled endpoint still gossips as controlled to remote viewers.\n    /// `default false` so a pre-#3 record parses.\n    #[serde(default)]\n    pub controlled: bool,\n    /// The number of read-only viewers currently attached to this endpoint's\n    /// session under the W2.5 controller/viewer model (REQ-RCVIEW-1). The BROKER\n    /// is the single writer (it owns the viewers map), stamped on every viewer\n    /// add/remove/evict; the picker renders `controlled by <driven_by> (+N\n    /// viewing)`. Absent / `0` ⇒ no viewers (or a pre-W2.5 record). Same\n    /// best-effort/self-heal caveat as `driven_by`. `default` so older records\n    /// still parse.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub viewer_count: Option<u32>,\n    /// The daemon-owned resting state (D9-2, REQ-INST-3): `\"active\"` /\n    /// `\"dormant\"` / `\"suspended\"` — the explicit instance state machine's\n    /// durable record (`spt_daemon::resting` owns the transitions; this field\n    /// is only ever written through [`set_rest_state`]). Distinct from\n    /// `status`: `status` is daemon **liveness** (online/offline, KH 2.5);\n    /// `rest_state` is the *resting model* of a live instance. Absent ⇒ a\n    /// pre-D9-2 / interim record (readers fall back to liveness-derived\n    /// status). `default` so older records still parse.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub rest_state: Option<String>,\n    /// When the instance entered dormancy (epoch ms) — the anchor the opt-in\n    /// `auto-suspend-after` threshold counts from (REQ-INST-3: counted from\n    /// dormancy onset, never from last activity). Present iff `rest_state`\n    /// is `\"dormant\"`; cleared on every other transition.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub dormant_since_ms: Option<u64>,\n    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of\n    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit\n    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint\n    /// even when the node enables it.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub auto_suspend_after_ms: Option<u64>,\n    /// The endpoint-authored resource-advertisement blurb (D9-4,\n    /// REQ-INST-14): the agent's own \"what I can serve\" line, written via\n    /// `spt resources set` ([`set_resources`]) and advertised into every\n    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level\n    /// `daemon.json` seed (if any) advertises instead — config seeds the\n    /// default, the agent refines its own at runtime (CONTEXT §resource\n    /// advertisement, both-authored).\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub resources: Option<String>,\n    /// The endpoint's **immutable home subnet** (D9-5, REQ-INST-15 —\n    /// ADR-0010): assigned at creation ([`crate::home::assign_home`]) or by\n    /// first-join adoption ([`crate::home::adopt_for_unset`]), then never\n    /// changed — **no setter exists**; `spt fork` is the cross-subnet move.\n    /// Absent ⇒ local-only (unpaired at creation, awaiting first join) or a\n    /// pre-D9-5 record. Creation seams must **carry this forward** on a\n    /// re-bind (a revive rewrites `info.json`; home survives by contract).\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub home_subnet: Option<String>,\n    /// The adapter chosen at creation (D9-5, REQ-INST-15 — the\n    /// adapter-at-creation rule): recorded when a creation/bind path knows\n    /// it, changed **only** via launch/resume-under-new (the bind paths are\n    /// the sole writers; no standalone setter). Absent ⇒ created by an\n    /// adapterless surface (`spt ready`) or a pre-D9-5 record.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub adapter: Option<String>,\n    /// The CURRENT psyche-host-failure state (v0.8.1,\n    /// REQ-HAZARD-LIVEHOST-BOOT-RACE): present ⇒ the brain's live-host\n    /// reconcile failed to spawn this perch's Psyche on its most recent\n    /// attempt (the boot-race / missing-psyche-binary failure that was\n    /// otherwise silent on the brain's stderr). Cleared (`None`) on a later\n    /// successful host, so a recovered live agent shows clean. `default` so\n    /// every pre-v0.8.1 record still parses (N-1-safe, additive). Set/cleared\n    /// only through [`set_psyche_host_error`].\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub psyche_host_error: Option<PsycheHostError>,\n    /// The most recent HOST-LEVEL failure for this endpoint (A-2,\n    /// REQ-WAKE-RESUME-LEG): present ⇒ the daemon's reconcile could not host/resume\n    /// it on its most recent attempt (e.g. the wake-resume leg's recorded adapter is\n    /// not registered). **Report-only, NEVER a liveness input** — neither\n    /// [`crate::liveness`] nor the A-1 effective-state derivation\n    /// (`advertised_status`) reads it, so a `host_error` beside `status=online`\n    /// still derives Active (the boot-race lesson: read-side truth stays\n    /// independent of a failure REPORT). Distinct from\n    /// [`psyche_host_error`](Self::psyche_host_error) (psyche-turn scoped, wrong\n    /// semantics for a host-level refusal). Cleared (`None`) on a successful\n    /// host/bind. `default` so every pre-A-2 record still parses (additive,\n    /// N-1-safe). Set/cleared only through [`set_host_error`].\n    // [impl->REQ-WAKE-RESUME-LEG]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub host_error: Option<String>,\n    /// Whether this endpoint is **broker-PTY-controllable** (REQ-PICKER-1): a\n    /// spt-hosted session the daemon spawned into a broker-owned PTY is `Some(true)`\n    /// (attachable/drivable); a harness-hosted LiveAgent (bound via `api listen` —\n    /// the harness owns the process, no broker PTY) is `Some(false)`. Stamped at the\n    /// establish seam (carry-forward on revive). `None` ⇒ a pre-v0.10.0 record, an\n    /// adapterless `ready`, or a non-live bind — the picker's status derivation\n    /// type-gates so only a LiveAgent ever renders the amber \"harness-only\" state.\n    /// `default` so every older record parses (N-1-safe, additive).\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub controllable: Option<bool>,\n    /// Whether this session's PTY input-writer thread is currently DROPPING input\n    /// because its bounded FIFO saturated — a genuinely wedged harness that has\n    /// stopped draining its stdin (v0.13.0 P0, REQ-HAZARD-PTY-INPUT-WRITER-WEDGE).\n    /// The BROKER is the single writer (it owns the per-session input-writer):\n    /// `Some(true)` is stamped on a full-queue DROP, cleared (`None`) on the next\n    /// accepted enqueue once the writer drains a slot (heal-on-resume). The daemon\n    /// NEVER wedges on a stuck harness — it drops + surfaces this instead. Absent ⇒\n    /// no backpressure (or a pre-P0 record). `default` so every older record parses\n    /// (N-1-safe, additive); same best-effort/self-heal caveat as `viewer_count`.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub input_backpressure: Option<bool>,\n    /// The CURRENT translation-binary fault surface (F029 C-1,\n    /// REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH): present ⇒ this spt-hosted\n    /// session's idle-delivery translation binary FAULTED (a real binary death —\n    /// stdin-write fail / stdout disconnect — or exhaustion of the commit-miss\n    /// strike budget) and idle inbound is SPOOLING until a working binary. The\n    /// string is the daemon's fault reason (the same text `TRANSLATION_FAULT`\n    /// logs to the F-019 unread daemon-stderr channel — surfaced here so a live\n    /// session's degraded idle delivery is harness-reachable, the honesty rule\n    /// F-027's ENDPOINT_SPAWN_FAIL established). The BROKER is the single writer:\n    /// stamped on a real fault, cleared (`None`) on a healthy respawn (or the\n    /// adapter-apply restart). A mere commit-deadline MISS is NOT a fault and\n    /// never stamps this (the binary is preserved; REQ-TRANSLATE-COMMIT-MISS-\n    /// TOLERANCE). Absent ⇒ no fault (or a pre-C-1 record). `default` so every\n    /// older record parses (N-1-safe, additive); same best-effort/self-heal\n    /// caveat as `input_backpressure`.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub translation_fault: Option<String>,\n…\n}\n…\n    fn write_read_round_trip() {\n…\n        // parent_pid omitted (None) → absent from JSON.\n        let raw = std::fs::read_to_string(d.path().join(\"info.json\")).unwrap();\n        assert!(\n            !raw.contains(\"parent_pid\"),\n            \"None field must be omitted: {raw}\"\n        );\n        assert!(raw.contains(\"\\\"state\\\":\\\"live_agent\\\"\"));\n    }\n\n    // [unit->REQ-HAZARD-INFO-JSON-TORN-READ] BUSY sentinel: full parse keeps it;\n    // read_pid yields None (no usable pid).\n    #[test]\n    fn busy_sentinel_round_trips_and_read_pid_is_none() {\n        let d = perch();\n        let info = InfoJson {\n            id: \"doyle\".to_string(),\n            started: \"t\".to_string(),\n            pid: PidValue::Busy(\"BUSY\".to_string()),\n            session_id: \"sid\".to_string(),\n            state: \"ready_agent\".to_string(),\n            parent_pid: None,\n            cwd: None,\n            status: None,\n            last_active_ms: None,\n            driven_by: None,\n            controlled: false,\n            viewer_count: None,\n            rest_state: None,\n            dormant_since_ms: None,\n            auto_suspend_after_ms: None,\n            resources: None,\n            home_subnet: None,\n            adapter: None,\n            psyche_host_error: None,\n            host_error: None,\n            controllable: None,\n            input_backpressure: None,\n            translation_fault: None,\n            read_env: std::collections::BTreeMap::new(),\n            agent_id: None,\n            agent_type: None,\n        };\n        write_info(d.path(), &info).unwrap();\n        assert_eq!(\n            read_info(d.path()).unwrap().pid,\n            PidValue::Busy(\"BUSY\".to_string())\n        );\n        assert_eq!(read_pid(d.path()), None, \"BUSY must yield no numeric pid\");\n    }\n\n    // [unit->REQ-PICKER-1] N-1-safe: a pre-v0.10.0 record with no `controllable`\n    // key parses clean to `controllable == None` (additive, serde-default), and a\n    // stamped value round-trips.\n    #[test]\n    fn controllable_is_additive_and_round_trips() {\n        let d = perch();\n…\n    }","startLine":20,"lineNumbers":[20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,null,258,null,611,null,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,null,687]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\info.rs"}}},"isError":false,"timestamp":1784197455176}}
{"type":"message","id":"12989544","parentId":"9dfc3580","timestamp":"2026-07-16T10:24:15.177Z","message":{"role":"toolResult","toolCallId":"call_3UvmdOzFUK7Bw8BhxodX6uD2|fc_017bff223290b7ba016a58b14f1d70819a9a277bc6dc9e4c4e","toolName":"read","content":[{"type":"text","text":"# Harness-hosted bringup: an adapter-agnostic seed + bind-time adapter/profile resolution\n\n## Status\n\naccepted (2026-06-16)\n\n## Context\n\nA harness-hosted SPT session (the harness binary — e.g. `claude` — is\nuser-launched and is spt's parent; today's Monitor model) brings a live (or\nready) agent up through the seed→listen handshake (CONTEXT §Startup flows,\nREQ-START-2): the SessionStart hook calls `spt api seed --pid <parent_pid>`, the\ndaemon holds an in-memory seed keyed by that pid, and the agent's\n`$LIVE start <id>` alias = `$SPT listen <id>` self-discovers its `parent_pid`,\nmatches the seed, binds the perch, and relays.\n\nTwo problems surfaced when **perri** drove `/sptc:live` on `spt-claude-code`\nv0.2.0 (2026-06-16):\n\n1. **`--adapter` is required on every `api` call** (`api/mod.rs`:\n   `ApiArgs.adapter: String`, non-optional). So both `seed` and `listen` are\n   forced to pass `--adapter claude-spt:live`. But on `listen` the flag is\n   **dead**: `bind_from_seed` already resolves the adapter *from the seed* by\n   `parent_pid` (`startup.rs`: \"the seed carries the creating adapter — the\n   adapter-at-creation rule\"), ignoring the flag. The mandatory flag breaks the\n   legacy-parity target — legacy `$LIVE start wall-a` → `$SPT listen wall-a`\n   carries no adapter — and pushes adapter-side complexity (the seed must be\n   passed an adapter; seed+listen must be chained in one background process so\n   they share a pid; etc.).\n2. **The seed snapshots a single adapter** at seed-time. A machine that hosts\n   more than one harness adapter for the same binary, or that `adapter add`s one\n   *after* SessionStart, cannot be served by a single generic SessionStart hook.\n\nThe desired end state: a SessionStart hook that is **identical for every adapter\nprofile** of a harness binary — `spt api seed --pid <pid> --session-id <sid>`,\nnothing adapter-specific — with the daemon resolving *which* adapter/profile owns\nthe session at the moment it binds.\n\n## Decision\n\n**1. The seed is adapter-agnostic.** `api seed` carries only `parent_pid` +\n`session_id` (+ optional `cwd`). No `adapter_name`. The seed is a pure *\"a\nharness session exists at this pid\"* record. (Removes the single-adapter snapshot\nand the `--adapter` requirement on `seed`.)\n\n**2. `--adapter` becomes an optional override** across the `api` group (an\nexplicit `name[:profile]` for adapter dev/iteration), never required. Omitted,\nthe adapter/profile is **resolved at bind** as a pure read against the live\nregistry — never a seed-time value that can drift. Applies to both LiveAgent\n(`listen`) and ReadyAgent (`poll`) bringup.\n\n**3. The match-key is a new `[adapter] host_binaries` manifest field** — the\nexecutable basenames a `kind=\"harness\"` adapter hosts agents inside (e.g.\n`host_binaries = [\"claude\"]`). Bind-time resolution:\n- the seed's `parent_pid` → that process's executable **basename**\n  (case-insensitive, `.exe`-stripped);\n- **candidate adapters** = registered harness adapters whose `host_binaries`\n  contains that basename;\n- zero candidates → a friendly error naming the binary and the `--adapter`\n  escape hatch.\n\n**4. Profile selection: an explicit pointer, with a `registered_at_ms`\nfallback.**\n- A durable **active-profile pointer** — `$SPT_HOME/adapters/active-profiles.toml`,\n  a flat `host_binary → \"adapter[:profile]\"` map — is the primary. Read at bind.\n- **Unset** → fall back to the candidate adapter with the greatest\n  `registered_at_ms` (the recorded registration timestamp on `record.toml`,\n  sturdier than fs-mtime), **base profile** (a specific *profile* is only ever\n  chosen by the pointer); name-ascending on ties. So a fresh single-adapter\n  install \"just works\" with no pointer.\n- The pointer is written **only** by `spt adapter use <adapter>[:profile]`\n  (`--clear` to drop). **Never auto-written** by install / update / `adapter add`\n  — that is precisely what would let an update silently flip the active profile.\n  A stale pointer (uninstalled adapter / deleted profile) self-heals: ignored,\n  fall back, warn once. Pruned on `adapter remove`.\n- Stored at the registry root, **sibling to the per-adapter `<name>/` dirs**, so\n  `adapter add/update/remove` (which only rewrite a `<name>/` subdir) can never\n  clobber it.\n\n**Storage asymmetry:** the **seed** is ephemeral (in-memory, daemon-held,\nper-session); the **pointer** is durable (on-disk, a standing user preference).\n\n## Consequences\n\n- Legacy parity restored: `$LIVE start <id>` → `$SPT listen <id>` with no\n  mandatory `--adapter`. A generic SessionStart hook —\n  `spt api seed --pid <pid> --session-id <sid>` — works for every profile of a\n  harness binary.\n- Adapter authors declare `host_binaries` and call `spt adapter use\n  <adapter>[:profile]` once per unique host binary they support (mdBook guides\n  this). Without a pointer, the freshest-registered matching adapter's base\n  profile is the default.\n- New surfaces are additive + N-1-safe: `host_binaries` is an optional manifest\n  field (omitted-serialized, like `shortcut_basename`); `active-profiles.toml` is\n  a new optional file; `--adapter` relaxes from required to optional (no break for\n  callers that still pass it).\n- An explicit profile choice survives adapter updates (the pointer is immune to\n  `registered_at_ms`/mtime churn) — the footgun pure-mtime selection would have\n  introduced is closed.\n\n## Alternatives considered\n\n- **Seed carries the matched-adapter *set*** (resolve at seed-time): rejected —\n  a seed written before an `adapter add` misses the new adapter; resolution as a\n  bind-time read is drift-free and strictly simpler.\n- **Derive the match-key from `[session.self]`** instead of a new field:\n  rejected — `[session.self]` is the *spt-launches-harness* command (spt-hosted\n  direction); harness-hosted is the inverse, so no existing token reliably names\n  the parent binary, and an explicit field gives a precise \"no adapter for this\n  binary\" error and multi-alias support.\n- **Pure fs-mtime selection + `touch` to change** (the first proposal):\n  rejected as the *primary* — `adapter update`/`add` rewrites manifests and bumps\n  mtime, silently flipping the active profile. Kept only as the spirit of the\n  unset-fallback, now hardened to `registered_at_ms` + an explicit pointer.\n- **Auto-write the pointer on install/update**: rejected — re-introduces the\n  update-churn footgun; absence-means-heuristic is the correct default.\n"}],"details":{"displayContent":{"text":"# Harness-hosted bringup: an adapter-agnostic seed + bind-time adapter/profile resolution\r\n\r\n## Status\r\n\r\naccepted (2026-06-16)\r\n\r\n## Context\r\n\r\nA harness-hosted SPT session (the harness binary — e.g. `claude` — is\r\nuser-launched and is spt's parent; today's Monitor model) brings a live (or\r\nready) agent up through the seed→listen handshake (CONTEXT §Startup flows,\r\nREQ-START-2): the SessionStart hook calls `spt api seed --pid <parent_pid>`, the\r\ndaemon holds an in-memory seed keyed by that pid, and the agent's\r\n`$LIVE start <id>` alias = `$SPT listen <id>` self-discovers its `parent_pid`,\r\nmatches the seed, binds the perch, and relays.\r\n\r\nTwo problems surfaced when **perri** drove `/sptc:live` on `spt-claude-code`\r\nv0.2.0 (2026-06-16):\r\n\r\n1. **`--adapter` is required on every `api` call** (`api/mod.rs`:\r\n   `ApiArgs.adapter: String`, non-optional). So both `seed` and `listen` are\r\n   forced to pass `--adapter claude-spt:live`. But on `listen` the flag is\r\n   **dead**: `bind_from_seed` already resolves the adapter *from the seed* by\r\n   `parent_pid` (`startup.rs`: \"the seed carries the creating adapter — the\r\n   adapter-at-creation rule\"), ignoring the flag. The mandatory flag breaks the\r\n   legacy-parity target — legacy `$LIVE start wall-a` → `$SPT listen wall-a`\r\n   carries no adapter — and pushes adapter-side complexity (the seed must be\r\n   passed an adapter; seed+listen must be chained in one background process so\r\n   they share a pid; etc.).\r\n2. **The seed snapshots a single adapter** at seed-time. A machine that hosts\r\n   more than one harness adapter for the same binary, or that `adapter add`s one\r\n   *after* SessionStart, cannot be served by a single generic SessionStart hook.\r\n\r\nThe desired end state: a SessionStart hook that is **identical for every adapter\r\nprofile** of a harness binary — `spt api seed --pid <pid> --session-id <sid>`,\r\nnothing adapter-specific — with the daemon resolving *which* adapter/profile owns\r\nthe session at the moment it binds.\r\n\r\n## Decision\r\n\r\n**1. The seed is adapter-agnostic.** `api seed` carries only `parent_pid` +\r\n`session_id` (+ optional `cwd`). No `adapter_name`. The seed is a pure *\"a\r\nharness session exists at this pid\"* record. (Removes the single-adapter snapshot\r\nand the `--adapter` requirement on `seed`.)\r\n\r\n**2. `--adapter` becomes an optional override** across the `api` group (an\r\nexplicit `name[:profile]` for adapter dev/iteration), never required. Omitted,\r\nthe adapter/profile is **resolved at bind** as a pure read against the live\r\nregistry — never a seed-time value that can drift. Applies to both LiveAgent\r\n(`listen`) and ReadyAgent (`poll`) bringup.\r\n\r\n**3. The match-key is a new `[adapter] host_binaries` manifest field** — the\r\nexecutable basenames a `kind=\"harness\"` adapter hosts agents inside (e.g.\r\n`host_binaries = [\"claude\"]`). Bind-time resolution:\r\n- the seed's `parent_pid` → that process's executable **basename**\r\n  (case-insensitive, `.exe`-stripped);\r\n- **candidate adapters** = registered harness adapters whose `host_binaries`\r\n  contains that basename;\r\n- zero candidates → a friendly error naming the binary and the `--adapter`\r\n  escape hatch.\r\n\r\n**4. Profile selection: an explicit pointer, with a `registered_at_ms`\r\nfallback.**\r\n- A durable **active-profile pointer** — `$SPT_HOME/adapters/active-profiles.toml`,\r\n  a flat `host_binary → \"adapter[:profile]\"` map — is the primary. Read at bind.\r\n- **Unset** → fall back to the candidate adapter with the greatest\r\n  `registered_at_ms` (the recorded registration timestamp on `record.toml`,\r\n  sturdier than fs-mtime), **base profile** (a specific *profile* is only ever\r\n  chosen by the pointer); name-ascending on ties. So a fresh single-adapter\r\n  install \"just works\" with no pointer.\r\n- The pointer is written **only** by `spt adapter use <adapter>[:profile]`\r\n  (`--clear` to drop). **Never auto-written** by install / update / `adapter add`\r\n  — that is precisely what would let an update silently flip the active profile.\r\n  A stale pointer (uninstalled adapter / deleted profile) self-heals: ignored,\r\n  fall back, warn once. Pruned on `adapter remove`.\r\n- Stored at the registry root, **sibling to the per-adapter `<name>/` dirs**, so\r\n  `adapter add/update/remove` (which only rewrite a `<name>/` subdir) can never\r\n  clobber it.\r\n\r\n**Storage asymmetry:** the **seed** is ephemeral (in-memory, daemon-held,\r\nper-session); the **pointer** is durable (on-disk, a standing user preference).\r\n\r\n## Consequences\r\n\r\n- Legacy parity restored: `$LIVE start <id>` → `$SPT listen <id>` with no\r\n  mandatory `--adapter`. A generic SessionStart hook —\r\n  `spt api seed --pid <pid> --session-id <sid>` — works for every profile of a\r\n  harness binary.\r\n- Adapter authors declare `host_binaries` and call `spt adapter use\r\n  <adapter>[:profile]` once per unique host binary they support (mdBook guides\r\n  this). Without a pointer, the freshest-registered matching adapter's base\r\n  profile is the default.\r\n- New surfaces are additive + N-1-safe: `host_binaries` is an optional manifest\r\n  field (omitted-serialized, like `shortcut_basename`); `active-profiles.toml` is\r\n  a new optional file; `--adapter` relaxes from required to optional (no break for\r\n  callers that still pass it).\r\n- An explicit profile choice survives adapter updates (the pointer is immune to\r\n  `registered_at_ms`/mtime churn) — the footgun pure-mtime selection would have\r\n  introduced is closed.\r\n\r\n## Alternatives considered\r\n\r\n- **Seed carries the matched-adapter *set*** (resolve at seed-time): rejected —\r\n  a seed written before an `adapter add` misses the new adapter; resolution as a\r\n  bind-time read is drift-free and strictly simpler.\r\n- **Derive the match-key from `[session.self]`** instead of a new field:\r\n  rejected — `[session.self]` is the *spt-launches-harness* command (spt-hosted\r\n  direction); harness-hosted is the inverse, so no existing token reliably names\r\n  the parent binary, and an explicit field gives a precise \"no adapter for this\r\n  binary\" error and multi-alias support.\r\n- **Pure fs-mtime selection + `touch` to change** (the first proposal):\r\n  rejected as the *primary* — `adapter update`/`add` rewrites manifests and bumps\r\n  mtime, silently flipping the active profile. Kept only as the spirit of the\r\n  unset-fallback, now hardened to `registered_at_ms` + an explicit pointer.\r\n- **Auto-write the pointer on install/update**: rejected — re-introduces the\r\n  update-churn footgun; absence-means-heuristic is the correct default.\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md"}}},"isError":false,"timestamp":1784197455177}}
{"type":"message","id":"d40b9ff3","parentId":"12989544","timestamp":"2026-07-16T10:24:15.183Z","message":{"role":"toolResult","toolCallId":"call_xs2KTgUaNp6qZGDmrRNKqjn7|fc_017bff223290b7ba016a58b14f1d5c819a89190e2739c31d9b","toolName":"read","content":[{"type":"text","text":"//! The broker↔brain message catalog: the kind discriminators and payload\n//! shapes that ride inside the forward-compatible [`Envelope`].\n//!\n//! Split out from [`crate::frame`] (which owns the *envelope mechanics* and the\n//! version handshake) so the message *vocabulary* can grow additively without\n//! touching the wire-compat machinery. Every payload is a plain serde struct\n//! with serde's default unknown-field tolerance, so a newer brain may add a\n//! field and an older broker still decodes the message (KNOWN-HAZARDS 2.3).\n//!\n//! Bytes (PTY input/output) ride as base64 strings — JSON has no native byte\n//! type, and base64 keeps the single envelope/codec path rather than forking a\n//! binary frame format.\n// [impl->REQ-DAEMON-2]\n\nuse base64::engine::general_purpose::STANDARD as B64;\nuse base64::Engine;\nuse serde::{Deserialize, Serialize};\n\nuse spt_net::net::attach::AttachIntent;\n\nuse crate::effect::Minter;\nuse crate::frame::Envelope;\n\n/// Brain→broker: spawn a child under a fresh broker-owned PTY.\npub const KIND_SPAWN: &str = \"spawn\";\n/// Broker→brain: a spawn succeeded; carries the assigned session id + child pid.\npub const KIND_SPAWNED: &str = \"spawned\";\n/// Brain→broker: (re)subscribe to a session's output from a given sequence.\npub const KIND_SUBSCRIBE: &str = \"subscribe\";\n/// Brain→broker: release this connection's controller/viewer role on a session\n/// (W2.5, REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when\n/// the operator's viewport ends, so `driven_by` / `viewer_count` clear promptly\n/// even if the serving brain connection lives on (a connection drop also clears\n/// the role, so this is idempotent belt-and-braces).\npub const KIND_UNSUBSCRIBE: &str = \"unsubscribe\";\n/// Brain→broker: write raw bytes to a session's input (send-keys).\npub const KIND_INPUT: &str = \"input\";\n/// Brain→broker: resize a session's PTY.\npub const KIND_RESIZE: &str = \"resize\";\n/// Brain→broker: terminate a session's child.\npub const KIND_KILL: &str = \"kill\";\n/// CLI→broker: apply a live adapter update to every session running the adapter\n/// (W3d, ADR-0025). Per affected endpoint the broker stops the resident\n/// translation child (releasing the OS file lock), CRC-swaps the install dir from\n/// the CLI-extracted staging tree, then restarts the translation from the NEW\n/// on-disk manifest path. Replies [`KIND_APPLIED`] (ok) or [`KIND_ERROR`].\npub const KIND_ADAPTER_APPLY: &str = \"adapter-apply\";\n/// Broker→brain: a chunk of a session's output (pushed as the child produces it).\npub const KIND_OUTPUT: &str = \"output\";\n/// Broker→brain: a session's child exited.\npub const KIND_EXIT: &str = \"exit\";\n/// Broker→brain: the outcome of a [`KIND_SUBSCRIBE`] under the W2.5\n/// controller/viewer model (REQ-RCVIEW-1 / REQ-KICK-1). Carries the resolved\n/// role — controller / viewer / busy-refused / took-control — so the serving\n/// brain can wake (controller-only), stay wake-neutral (viewer), or refuse the\n/// operator (busy). A normal reply, not a new wire frame (the busy refusal is\n/// rendered client-side by `rc` from the endpoint's `driven_by`).\npub const KIND_SUBSCRIBED: &str = \"subscribed\";\n/// Broker→brain: the current PTY size of a session, pushed to a VIEWER on\n/// attach and to every viewer on a controller resize (REQ-RCVIEW-1). The\n/// serving brain forwards it as a [`spt_net::net::attach::AttachRecord::Size`]\n/// so the viewer letterboxes client-side (never resizing the PTY itself —\n/// resize is controller-exclusive).\npub const KIND_SIZE: &str = \"size\";\n/// Broker→brain: you (a controller) have been DISPLACED by a different operator's\n/// explicit `--take`/`kick` (REQ-KICK-1). The serving brain forwards this as a\n/// loud [`spt_net::net::attach::AttachRecord::Displaced`] on the wire, then\n/// detaches. Fired ONLY on a genuine cross-operator control-ownership change —\n/// NEVER on a same-origin re-serve after a brain restart (the gate-#7 self-kick\n/// trap: a successor re-taking the controller slot with the SAME `by` is silent).\npub const KIND_DISPLACED: &str = \"displaced\";\n/// Broker→brain(VIEWER): this viewer's bounded subscription queue overflowed and\n/// it was EVICTED (REQ-HAZARD-VIEWER-ISOLATION session-protection). DISTINCT from\n/// a session-exit EOF: the serving brain must NOT tear the attach down — it\n/// re-subscribes from the current ring floor (SKIP-TO-LIVE,\n/// REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). Written by the viewer's dedicated writer\n/// thread (OFF the log lock / off the drain) as its final frame.\npub const KIND_VIEWER_EVICTED: &str = \"viewer_evicted\";\n/// Broker→brain: an idempotent effect (an `input` carrying an `op_id`) is durably\n/// applied **or** was already applied (deduped). The exactly-once ack: a brain\n/// learns its effect landed, and a replay after a crash is acknowledged the same\n/// way so the brain can retire it (B5 / KNOWN-HAZARDS 7.2).\npub const KIND_APPLIED: &str = \"applied\";\n/// Broker→brain: a command failed (carries an operator-facing message).\npub const KIND_ERROR: &str = \"error\";\n/// Brain→broker: report the broker-owned net endpoint's status (D4a). The brain\n/// holds no endpoint of its own — the broker owns the node's network presence\n/// (ADR-0004 §B), so status (node id, dialable address, conn count) is an IPC ask.\npub const KIND_NET_STATUS: &str = \"net-status\";\n/// Broker→brain: the net endpoint status reply.\npub const KIND_NET_STATUS_REPLY: &str = \"net-status-reply\";\n/// Brain→broker: report the broker's OWN compiled image version (F-025). The\n/// broker SURVIVES `spt update apply` (only the brain restarts — ADR-0018 D3-3),\n/// so a freshly-applied node may still be RUNNING an older broker binary; the\n/// broker is the authoritative source for \"what version is actually executing\",\n/// answered from its own compile-time constant — NEVER inferred from disk. An\n/// older broker that predates this kind replies `unknown command kind` (the\n/// router's forward-compat default), which the CLI reads as \"too old to report\".\npub const KIND_BROKER_IMAGE: &str = \"broker-image\";\n/// Broker→brain: the broker image version reply.\npub const KIND_BROKER_IMAGE_REPLY: &str = \"broker-image-reply\";\n/// Brain→broker query: the brain-subscriber stall-evict tally (LIFECYCLE-TRUTH W2\n/// observability rider, REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — how many\n/// controllers were stall-evicted for a suspended/black-holed brain conn since\n/// boot, and when the most recent fired. Surfaced on `daemon status` so a brain\n/// that never returns is a diagnosable fact. An older broker predating this kind\n/// replies `unknown command kind` (forward-compat), read as \"not reported\".\npub const KIND_STALL_EVICTS: &str = \"stall-evicts\";\n/// Broker→brain: the stall-evict tally reply.\npub const KIND_STALL_EVICTS_REPLY: &str = \"stall-evicts-reply\";\n/// Brain→broker: dial a peer over the broker-owned endpoint (D4a). With an\n/// `op_id` the dial is exactly-once (journaled like an idempotent `input`).\npub const KIND_NET_DIAL: &str = \"net-dial\";\n/// Brain→broker: mint (or reuse) the broker's single in-process loopback\n/// connection (M12 W1.5, B1) — the transport a LOCAL `spt rc` rides because QUIC\n/// self-dial is refused by iroh. An explicit verb (NOT a sentinel addr on\n/// [`KIND_NET_DIAL`]): loopback handling stays isolated from the dial hot path.\n/// Null payload (the singleton is idempotent by reuse — no op id, no addr).\n/// Answered with a [`KIND_NET_DIALED`] whose `remote_id_hex` is THIS node's own id.\npub const KIND_NET_DIAL_LOOPBACK: &str = \"net-dial-loopback\";\n/// Broker→brain: a dial durably landed — fresh or deduped (`applied_now`\n/// distinguishes, mirroring [`KIND_APPLIED`]). Carries the broker-minted conn id.\npub const KIND_NET_DIALED: &str = \"net-dialed\";\n/// Brain→broker: open a bidi QUIC stream on a held connection (D4b). With an\n/// `op_id` the open is exactly-once (journaled like a dial).\npub const KIND_NET_STREAM_OPEN: &str = \"net-stream-open\";\n/// Broker→brain: a stream open durably landed (fresh or deduped).\npub const KIND_NET_STREAM_OPENED: &str = \"net-stream-opened\";\n/// Brain→broker: write bytes to a held stream's send side (D4b). With an\n/// `op_id` the write is exactly-once (journaled; acked with [`KIND_NET_SENT`]).\npub const KIND_NET_STREAM_SEND: &str = \"net-stream-send\";\n/// Broker→brain: a journaled stream send durably landed (fresh or deduped).\npub const KIND_NET_SENT: &str = \"net-sent\";\n/// Brain→broker: list the streams the broker currently holds (D4b). The query\n/// model a re-attaching brain uses to find streams that arrived while it was\n/// down — no announce buffer to miss.\npub const KIND_NET_STREAMS: &str = \"net-streams\";\n/// Broker→brain: the held-streams reply.\npub const KIND_NET_STREAMS_REPLY: &str = \"net-streams-reply\";\n/// Brain→broker: (re)subscribe to a stream's buffered reads from a cursor —\n/// the PTY `subscribe` discipline applied to a QUIC stream (gapless resume).\npub const KIND_NET_STREAM_SUBSCRIBE: &str = \"net-stream-subscribe\";\n/// Broker→brain: a sequenced chunk of a stream's reads (live or replayed).\npub const KIND_NET_STREAM_DATA: &str = \"net-stream-data\";\n/// Broker→brain: a stream's read side finished cleanly; `next_seq` tells the\n/// brain the final cursor so it can verify it missed nothing.\npub const KIND_NET_STREAM_EOF: &str = \"net-stream-eof\";\n/// Brain→broker: (re)subscribe to the broker's conn-liveness event log from a\n/// cursor (D4c) — the PresenceChannel broker seam. Same gapless-resume\n/// discipline as a stream subscribe.\n// [impl->REQ-EP-4]\npub const KIND_NET_PRESENCE_SUBSCRIBE: &str = \"net-presence-subscribe\";\n/// Broker→brain: one sequenced conn-liveness event (live or replayed).\npub const KIND_NET_PRESENCE_EVENT: &str = \"net-presence-event\";\n/// Brain→broker (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION): submit a NON-BLOCKING\n/// dial. Payload = the existing [`NetDialReq`] (addr + op_id + minter). The\n/// broker acks immediately ([`KIND_NET_DIAL_SUBMITTED`]) and dials CONCURRENTLY\n/// on its runtime under W1's per-dial bound; the OUTCOME rides the presence\n/// stream ([`PRESENCE_CONNECTED`] on success, [`PRESENCE_DIAL_FAILED`] on\n/// failure) — never a blocking reply. Contrast [`KIND_NET_DIAL`] (blocking\n/// request-reply, kept for its other callers: shellwake, pairing, rc).\n// [impl->REQ-PUMP-PEER-ISOLATION]\npub const KIND_NET_DIAL_SUBMIT: &str = \"net-dial-submit\";\n/// Broker→brain (PUMP-TRUTH W2): the immediate ack that a [`KIND_NET_DIAL_SUBMIT`]\n/// was accepted and the concurrent dial spawned. Bare ack (null payload) — the\n/// dial's real outcome rides the presence stream, not this frame.\n// [impl->REQ-PUMP-PEER-ISOLATION]\npub const KIND_NET_DIAL_SUBMITTED: &str = \"net-dial-submitted\";\n\n/// [`NetPresenceEvent::kind`]: a connection joined the broker's conn table.\npub const PRESENCE_CONNECTED: &str = \"connected\";\n/// [`NetPresenceEvent::kind`]: a connection ended and left the conn table.\npub const PRESENCE_DISCONNECTED: &str = \"disconnected\";\n/// [`NetPresenceEvent::kind`] (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION): a\n/// NON-BLOCKING dial submitted via [`KIND_NET_DIAL_SUBMIT`] failed to connect.\n/// The outcome-channel for a fire-and-forget dial (its success rides\n/// [`PRESENCE_CONNECTED`] as today). Carries no live connection → `conn_id: 0`\n/// sentinel; consumers correlate a fail by `kind` + `remote_id_hex`, NEVER\n/// `conn_id`. Additive kind — an older brain tolerates it (the `kind` string is\n/// forward-compat by design, 2.3).\n// [impl->REQ-PUMP-PEER-ISOLATION]\npub const PRESENCE_DIAL_FAILED: &str = \"dial-failed\";\n\n/// Brain→broker: run the joiner leg of the subnet pairing ceremony (M7 D3,\n/// REQ-SUBNET-2). The ceremony must run broker-side: it dials via the\n/// rendezvous and binds/pins the broker-owned endpoint's REAL node pubkey.\npub const KIND_PAIR_JOIN: &str = \"pair-join\";\n/// Broker→brain: the join ceremony's outcome (success or classified failure).\npub const KIND_PAIR_JOINED: &str = \"pair-joined\";\n\n/// Brain→broker: phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE) — MEET a\n/// seed-holder for the named subnet (no code). The daemon resolves and holds the\n/// member's real pairing address, replying [`KIND_MET_MEMBER`].\npub const KIND_PAIR_MEET: &str = \"pair-meet\";\n/// Broker→brain: a seed-holder was met — its real address is held under a\n/// session id, awaiting the code ([`KIND_PAIR_CODE_SUBMIT`]).\npub const KIND_MET_MEMBER: &str = \"met-member\";\n/// Brain→broker: phase 2 of the two-phase join — submit the code for a held\n/// meet session. The daemon dials the HELD address (no re-search) and runs the\n/// SPAKE2 ceremony, replying [`KIND_PAIR_JOINED`]. A wrong-code retry re-submits\n/// against the same session id (ceremony only).\npub const KIND_PAIR_CODE_SUBMIT: &str = \"pair-code-submit\";\n\n/// Brain→broker: list hosted sessions (id + owning endpoint). The inbound\n/// dispatcher's session→endpoint resolution (D9-1).\npub const KIND_SESSIONS: &str = \"sessions\";\n/// Broker→brain reply to [`KIND_SESSIONS`].\npub const KIND_SESSIONS_REPLY: &str = \"sessions-reply\";\n\n/// Client→broker: deliver an inbound message to an spt-hosted endpoint by\n/// **endpoint id** — the broker resolves the endpoint to its hosted session and\n/// injects the bytes straight into that session's PTY (REQ-SEND-SPT-HOSTED,\n/// CONTEXT:39/187-188 \"the daemon injects events straight into the PTY\"). The\n/// endpoint-keyed counterpart of [`KIND_INPUT`] (which addresses a session by the\n/// broker-minted u64 a CLI can't know): atomic endpoint→session resolution under\n/// the broker's lock, one round-trip, no TOCTOU. Fire-and-forget (no `op_id`,\n/// no journal — a user message is delivered like the relay's `<EVENT>` emit, not\n/// an exactly-once effect).\npub const KIND_ENDPOINT_INPUT: &str = \"endpoint-input\";\n/// Broker→client reply to [`KIND_ENDPOINT_INPUT`]: whether a hosted session for\n/// the endpoint was found and the bytes were written to its PTY. `delivered=false`\n/// (no matching broker session) tells the caller to fall back to the spool.\npub const KIND_ENDPOINT_INJECTED: &str = \"endpoint-injected\";\n\n/// Client→broker: trigger a **planned brain-process restart** (ADR-0018 D3-3,\n/// Q3). `spt update apply` sends this after swapping the binary on disk; the\n/// broker signals its brain supervisor to cycle the brain child with\n/// `start-reason=update`, so the live brain restarts onto the new binary while\n/// every broker-held endpoint stays untouched at the process level. Additive\n/// verb (KH-2.3): an older broker that predates it answers with a generic error,\n/// which the caller surfaces — it never silently no-ops a swapped binary.\npub const KIND_BRAIN_RESTART: &str = \"brain-restart\";\n/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\n/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\n/// acceptance, not completion. `honored` is false when the broker has no\n/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\n/// reached a broker that cannot itself respawn.\npub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\n\n/// `spawn` payload — the program + args + initial geometry. The broker spawns\n/// it under a PTY (no shell interpretation; argv is passed through).\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct SpawnReq {\n    /// The program to exec (resolved on `PATH` by the OS spawn).\n    pub program: String,\n    /// Its arguments, verbatim.\n    #[serde(default)]\n    pub args: Vec<String>,\n    /// Initial PTY rows.\n    pub rows: u16,\n    /// Initial PTY cols.\n    pub cols: u16,\n    /// The endpoint this session belongs to (D9-1: the remote-drive\n    /// dispatcher's gate subject — `serve_attach` gates on the *owning*\n    /// endpoint's whitelist, resolved from the broker's table, never from\n    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher\n    /// refuses to serve an unlabeled session, fail-closed.\n    #[serde(default)]\n    pub endpoint: String,\n    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`\n    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup\n    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\n    /// consumer (Claude Code) reads its `.claude`, session history, and digest\n…\n}\n\n[Showing lines 1-263 of 1478. Use :264 to continue]"}],"details":{"truncation":{"content":"//! The broker↔brain message catalog: the kind discriminators and payload\r\n//! shapes that ride inside the forward-compatible [`Envelope`].\r\n//!\r\n//! Split out from [`crate::frame`] (which owns the *envelope mechanics* and the\r\n//! version handshake) so the message *vocabulary* can grow additively without\r\n//! touching the wire-compat machinery. Every payload is a plain serde struct\r\n//! with serde's default unknown-field tolerance, so a newer brain may add a\r\n//! field and an older broker still decodes the message (KNOWN-HAZARDS 2.3).\r\n//!\r\n//! Bytes (PTY input/output) ride as base64 strings — JSON has no native byte\r\n//! type, and base64 keeps the single envelope/codec path rather than forking a\r\n//! binary frame format.\r\n// [impl->REQ-DAEMON-2]\r\n\r\nuse base64::engine::general_purpose::STANDARD as B64;\r\nuse base64::Engine;\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse spt_net::net::attach::AttachIntent;\r\n\r\nuse crate::effect::Minter;\r\nuse crate::frame::Envelope;\r\n\r\n/// Brain→broker: spawn a child under a fresh broker-owned PTY.\r\npub const KIND_SPAWN: &str = \"spawn\";\r\n/// Broker→brain: a spawn succeeded; carries the assigned session id + child pid.\r\npub const KIND_SPAWNED: &str = \"spawned\";\r\n/// Brain→broker: (re)subscribe to a session's output from a given sequence.\r\npub const KIND_SUBSCRIBE: &str = \"subscribe\";\r\n/// Brain→broker: release this connection's controller/viewer role on a session\r\n/// (W2.5, REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when\r\n/// the operator's viewport ends, so `driven_by` / `viewer_count` clear promptly\r\n/// even if the serving brain connection lives on (a connection drop also clears\r\n/// the role, so this is idempotent belt-and-braces).\r\npub const KIND_UNSUBSCRIBE: &str = \"unsubscribe\";\r\n/// Brain→broker: write raw bytes to a session's input (send-keys).\r\npub const KIND_INPUT: &str = \"input\";\r\n/// Brain→broker: resize a session's PTY.\r\npub const KIND_RESIZE: &str = \"resize\";\r\n/// Brain→broker: terminate a session's child.\r\npub const KIND_KILL: &str = \"kill\";\r\n/// CLI→broker: apply a live adapter update to every session running the adapter\r\n/// (W3d, ADR-0025). Per affected endpoint the broker stops the resident\r\n/// translation child (releasing the OS file lock), CRC-swaps the install dir from\r\n/// the CLI-extracted staging tree, then restarts the translation from the NEW\r\n/// on-disk manifest path. Replies [`KIND_APPLIED`] (ok) or [`KIND_ERROR`].\r\npub const KIND_ADAPTER_APPLY: &str = \"adapter-apply\";\r\n/// Broker→brain: a chunk of a session's output (pushed as the child produces it).\r\npub const KIND_OUTPUT: &str = \"output\";\r\n/// Broker→brain: a session's child exited.\r\npub const KIND_EXIT: &str = \"exit\";\r\n/// Broker→brain: the outcome of a [`KIND_SUBSCRIBE`] under the W2.5\r\n/// controller/viewer model (REQ-RCVIEW-1 / REQ-KICK-1). Carries the resolved\r\n/// role — controller / viewer / busy-refused / took-control — so the serving\r\n/// brain can wake (controller-only), stay wake-neutral (viewer), or refuse the\r\n/// operator (busy). A normal reply, not a new wire frame (the busy refusal is\r\n/// rendered client-side by `rc` from the endpoint's `driven_by`).\r\npub const KIND_SUBSCRIBED: &str = \"subscribed\";\r\n/// Broker→brain: the current PTY size of a session, pushed to a VIEWER on\r\n/// attach and to every viewer on a controller resize (REQ-RCVIEW-1). The\r\n/// serving brain forwards it as a [`spt_net::net::attach::AttachRecord::Size`]\r\n/// so the viewer letterboxes client-side (never resizing the PTY itself —\r\n/// resize is controller-exclusive).\r\npub const KIND_SIZE: &str = \"size\";\r\n/// Broker→brain: you (a controller) have been DISPLACED by a different operator's\r\n/// explicit `--take`/`kick` (REQ-KICK-1). The serving brain forwards this as a\r\n/// loud [`spt_net::net::attach::AttachRecord::Displaced`] on the wire, then\r\n/// detaches. Fired ONLY on a genuine cross-operator control-ownership change —\r\n/// NEVER on a same-origin re-serve after a brain restart (the gate-#7 self-kick\r\n/// trap: a successor re-taking the controller slot with the SAME `by` is silent).\r\npub const KIND_DISPLACED: &str = \"displaced\";\r\n/// Broker→brain(VIEWER): this viewer's bounded subscription queue overflowed and\r\n/// it was EVICTED (REQ-HAZARD-VIEWER-ISOLATION session-protection). DISTINCT from\r\n/// a session-exit EOF: the serving brain must NOT tear the attach down — it\r\n/// re-subscribes from the current ring floor (SKIP-TO-LIVE,\r\n/// REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). Written by the viewer's dedicated writer\r\n/// thread (OFF the log lock / off the drain) as its final frame.\r\npub const KIND_VIEWER_EVICTED: &str = \"viewer_evicted\";\r\n/// Broker→brain: an idempotent effect (an `input` carrying an `op_id`) is durably\r\n/// applied **or** was already applied (deduped). The exactly-once ack: a brain\r\n/// learns its effect landed, and a replay after a crash is acknowledged the same\r\n/// way so the brain can retire it (B5 / KNOWN-HAZARDS 7.2).\r\npub const KIND_APPLIED: &str = \"applied\";\r\n/// Broker→brain: a command failed (carries an operator-facing message).\r\npub const KIND_ERROR: &str = \"error\";\r\n/// Brain→broker: report the broker-owned net endpoint's status (D4a). The brain\r\n/// holds no endpoint of its own — the broker owns the node's network presence\r\n/// (ADR-0004 §B), so status (node id, dialable address, conn count) is an IPC ask.\r\npub const KIND_NET_STATUS: &str = \"net-status\";\r\n/// Broker→brain: the net endpoint status reply.\r\npub const KIND_NET_STATUS_REPLY: &str = \"net-status-reply\";\r\n/// Brain→broker: report the broker's OWN compiled image version (F-025). The\r\n/// broker SURVIVES `spt update apply` (only the brain restarts — ADR-0018 D3-3),\r\n/// so a freshly-applied node may still be RUNNING an older broker binary; the\r\n/// broker is the authoritative source for \"what version is actually executing\",\r\n/// answered from its own compile-time constant — NEVER inferred from disk. An\r\n/// older broker that predates this kind replies `unknown command kind` (the\r\n/// router's forward-compat default), which the CLI reads as \"too old to report\".\r\npub const KIND_BROKER_IMAGE: &str = \"broker-image\";\r\n/// Broker→brain: the broker image version reply.\r\npub const KIND_BROKER_IMAGE_REPLY: &str = \"broker-image-reply\";\r\n/// Brain→broker query: the brain-subscriber stall-evict tally (LIFECYCLE-TRUTH W2\r\n/// observability rider, REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — how many\r\n/// controllers were stall-evicted for a suspended/black-holed brain conn since\r\n/// boot, and when the most recent fired. Surfaced on `daemon status` so a brain\r\n/// that never returns is a diagnosable fact. An older broker predating this kind\r\n/// replies `unknown command kind` (forward-compat), read as \"not reported\".\r\npub const KIND_STALL_EVICTS: &str = \"stall-evicts\";\r\n/// Broker→brain: the stall-evict tally reply.\r\npub const KIND_STALL_EVICTS_REPLY: &str = \"stall-evicts-reply\";\r\n/// Brain→broker: dial a peer over the broker-owned endpoint (D4a). With an\r\n/// `op_id` the dial is exactly-once (journaled like an idempotent `input`).\r\npub const KIND_NET_DIAL: &str = \"net-dial\";\r\n/// Brain→broker: mint (or reuse) the broker's single in-process loopback\r\n/// connection (M12 W1.5, B1) — the transport a LOCAL `spt rc` rides because QUIC\r\n/// self-dial is refused by iroh. An explicit verb (NOT a sentinel addr on\r\n/// [`KIND_NET_DIAL`]): loopback handling stays isolated from the dial hot path.\r\n/// Null payload (the singleton is idempotent by reuse — no op id, no addr).\r\n/// Answered with a [`KIND_NET_DIALED`] whose `remote_id_hex` is THIS node's own id.\r\npub const KIND_NET_DIAL_LOOPBACK: &str = \"net-dial-loopback\";\r\n/// Broker→brain: a dial durably landed — fresh or deduped (`applied_now`\r\n/// distinguishes, mirroring [`KIND_APPLIED`]). Carries the broker-minted conn id.\r\npub const KIND_NET_DIALED: &str = \"net-dialed\";\r\n/// Brain→broker: open a bidi QUIC stream on a held connection (D4b). With an\r\n/// `op_id` the open is exactly-once (journaled like a dial).\r\npub const KIND_NET_STREAM_OPEN: &str = \"net-stream-open\";\r\n/// Broker→brain: a stream open durably landed (fresh or deduped).\r\npub const KIND_NET_STREAM_OPENED: &str = \"net-stream-opened\";\r\n/// Brain→broker: write bytes to a held stream's send side (D4b). With an\r\n/// `op_id` the write is exactly-once (journaled; acked with [`KIND_NET_SENT`]).\r\npub const KIND_NET_STREAM_SEND: &str = \"net-stream-send\";\r\n/// Broker→brain: a journaled stream send durably landed (fresh or deduped).\r\npub const KIND_NET_SENT: &str = \"net-sent\";\r\n/// Brain→broker: list the streams the broker currently holds (D4b). The query\r\n/// model a re-attaching brain uses to find streams that arrived while it was\r\n/// down — no announce buffer to miss.\r\npub const KIND_NET_STREAMS: &str = \"net-streams\";\r\n/// Broker→brain: the held-streams reply.\r\npub const KIND_NET_STREAMS_REPLY: &str = \"net-streams-reply\";\r\n/// Brain→broker: (re)subscribe to a stream's buffered reads from a cursor —\r\n/// the PTY `subscribe` discipline applied to a QUIC stream (gapless resume).\r\npub const KIND_NET_STREAM_SUBSCRIBE: &str = \"net-stream-subscribe\";\r\n/// Broker→brain: a sequenced chunk of a stream's reads (live or replayed).\r\npub const KIND_NET_STREAM_DATA: &str = \"net-stream-data\";\r\n/// Broker→brain: a stream's read side finished cleanly; `next_seq` tells the\r\n/// brain the final cursor so it can verify it missed nothing.\r\npub const KIND_NET_STREAM_EOF: &str = \"net-stream-eof\";\r\n/// Brain→broker: (re)subscribe to the broker's conn-liveness event log from a\r\n/// cursor (D4c) — the PresenceChannel broker seam. Same gapless-resume\r\n/// discipline as a stream subscribe.\r\n// [impl->REQ-EP-4]\r\npub const KIND_NET_PRESENCE_SUBSCRIBE: &str = \"net-presence-subscribe\";\r\n/// Broker→brain: one sequenced conn-liveness event (live or replayed).\r\npub const KIND_NET_PRESENCE_EVENT: &str = \"net-presence-event\";\r\n/// Brain→broker (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION): submit a NON-BLOCKING\r\n/// dial. Payload = the existing [`NetDialReq`] (addr + op_id + minter). The\r\n/// broker acks immediately ([`KIND_NET_DIAL_SUBMITTED`]) and dials CONCURRENTLY\r\n/// on its runtime under W1's per-dial bound; the OUTCOME rides the presence\r\n/// stream ([`PRESENCE_CONNECTED`] on success, [`PRESENCE_DIAL_FAILED`] on\r\n/// failure) — never a blocking reply. Contrast [`KIND_NET_DIAL`] (blocking\r\n/// request-reply, kept for its other callers: shellwake, pairing, rc).\r\n// [impl->REQ-PUMP-PEER-ISOLATION]\r\npub const KIND_NET_DIAL_SUBMIT: &str = \"net-dial-submit\";\r\n/// Broker→brain (PUMP-TRUTH W2): the immediate ack that a [`KIND_NET_DIAL_SUBMIT`]\r\n/// was accepted and the concurrent dial spawned. Bare ack (null payload) — the\r\n/// dial's real outcome rides the presence stream, not this frame.\r\n// [impl->REQ-PUMP-PEER-ISOLATION]\r\npub const KIND_NET_DIAL_SUBMITTED: &str = \"net-dial-submitted\";\r\n\r\n/// [`NetPresenceEvent::kind`]: a connection joined the broker's conn table.\r\npub const PRESENCE_CONNECTED: &str = \"connected\";\r\n/// [`NetPresenceEvent::kind`]: a connection ended and left the conn table.\r\npub const PRESENCE_DISCONNECTED: &str = \"disconnected\";\r\n/// [`NetPresenceEvent::kind`] (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION): a\r\n/// NON-BLOCKING dial submitted via [`KIND_NET_DIAL_SUBMIT`] failed to connect.\r\n/// The outcome-channel for a fire-and-forget dial (its success rides\r\n/// [`PRESENCE_CONNECTED`] as today). Carries no live connection → `conn_id: 0`\r\n/// sentinel; consumers correlate a fail by `kind` + `remote_id_hex`, NEVER\r\n/// `conn_id`. Additive kind — an older brain tolerates it (the `kind` string is\r\n/// forward-compat by design, 2.3).\r\n// [impl->REQ-PUMP-PEER-ISOLATION]\r\npub const PRESENCE_DIAL_FAILED: &str = \"dial-failed\";\r\n\r\n/// Brain→broker: run the joiner leg of the subnet pairing ceremony (M7 D3,\r\n/// REQ-SUBNET-2). The ceremony must run broker-side: it dials via the\r\n/// rendezvous and binds/pins the broker-owned endpoint's REAL node pubkey.\r\npub const KIND_PAIR_JOIN: &str = \"pair-join\";\r\n/// Broker→brain: the join ceremony's outcome (success or classified failure).\r\npub const KIND_PAIR_JOINED: &str = \"pair-joined\";\r\n\r\n/// Brain→broker: phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE) — MEET a\r\n/// seed-holder for the named subnet (no code). The daemon resolves and holds the\r\n/// member's real pairing address, replying [`KIND_MET_MEMBER`].\r\npub const KIND_PAIR_MEET: &str = \"pair-meet\";\r\n/// Broker→brain: a seed-holder was met — its real address is held under a\r\n/// session id, awaiting the code ([`KIND_PAIR_CODE_SUBMIT`]).\r\npub const KIND_MET_MEMBER: &str = \"met-member\";\r\n/// Brain→broker: phase 2 of the two-phase join — submit the code for a held\r\n/// meet session. The daemon dials the HELD address (no re-search) and runs the\r\n/// SPAKE2 ceremony, replying [`KIND_PAIR_JOINED`]. A wrong-code retry re-submits\r\n/// against the same session id (ceremony only).\r\npub const KIND_PAIR_CODE_SUBMIT: &str = \"pair-code-submit\";\r\n\r\n/// Brain→broker: list hosted sessions (id + owning endpoint). The inbound\r\n/// dispatcher's session→endpoint resolution (D9-1).\r\npub const KIND_SESSIONS: &str = \"sessions\";\r\n/// Broker→brain reply to [`KIND_SESSIONS`].\r\npub const KIND_SESSIONS_REPLY: &str = \"sessions-reply\";\r\n\r\n/// Client→broker: deliver an inbound message to an spt-hosted endpoint by\r\n/// **endpoint id** — the broker resolves the endpoint to its hosted session and\r\n/// injects the bytes straight into that session's PTY (REQ-SEND-SPT-HOSTED,\r\n/// CONTEXT:39/187-188 \"the daemon injects events straight into the PTY\"). The\r\n/// endpoint-keyed counterpart of [`KIND_INPUT`] (which addresses a session by the\r\n/// broker-minted u64 a CLI can't know): atomic endpoint→session resolution under\r\n/// the broker's lock, one round-trip, no TOCTOU. Fire-and-forget (no `op_id`,\r\n/// no journal — a user message is delivered like the relay's `<EVENT>` emit, not\r\n/// an exactly-once effect).\r\npub const KIND_ENDPOINT_INPUT: &str = \"endpoint-input\";\r\n/// Broker→client reply to [`KIND_ENDPOINT_INPUT`]: whether a hosted session for\r\n/// the endpoint was found and the bytes were written to its PTY. `delivered=false`\r\n/// (no matching broker session) tells the caller to fall back to the spool.\r\npub const KIND_ENDPOINT_INJECTED: &str = \"endpoint-injected\";\r\n\r\n/// Client→broker: trigger a **planned brain-process restart** (ADR-0018 D3-3,\r\n/// Q3). `spt update apply` sends this after swapping the binary on disk; the\r\n/// broker signals its brain supervisor to cycle the brain child with\r\n/// `start-reason=update`, so the live brain restarts onto the new binary while\r\n/// every broker-held endpoint stays untouched at the process level. Additive\r\n/// verb (KH-2.3): an older broker that predates it answers with a generic error,\r\n/// which the caller surfaces — it never silently no-ops a swapped binary.\r\npub const KIND_BRAIN_RESTART: &str = \"brain-restart\";\r\n/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\r\n/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\r\n/// acceptance, not completion. `honored` is false when the broker has no\r\n/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\r\n/// reached a broker that cannot itself respawn.\r\npub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\r\n\r\n/// `spawn` payload — the program + args + initial geometry. The broker spawns\r\n/// it under a PTY (no shell interpretation; argv is passed through).\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\npub struct SpawnReq {\r\n    /// The program to exec (resolved on `PATH` by the OS spawn).\r\n    pub program: String,\r\n    /// Its arguments, verbatim.\r\n    #[serde(default)]\r\n    pub args: Vec<String>,\r\n    /// Initial PTY rows.\r\n    pub rows: u16,\r\n    /// Initial PTY cols.\r\n    pub cols: u16,\r\n    /// The endpoint this session belongs to (D9-1: the remote-drive\r\n    /// dispatcher's gate subject — `serve_attach` gates on the *owning*\r\n    /// endpoint's whitelist, resolved from the broker's table, never from\r\n    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher\r\n    /// refuses to serve an unlabeled session, fail-closed.\r\n    #[serde(default)]\r\n    pub endpoint: String,\r\n    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`\r\n    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup\r\n    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\r\n    /// consumer (Claude Code) reads its `.claude`, session history, and digest\r","truncated":true,"truncatedBy":"lines","totalLines":1478,"totalBytes":16674,"outputLines":263,"outputBytes":16674,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! The broker↔brain message catalog: the kind discriminators and payload\r\n//! shapes that ride inside the forward-compatible [`Envelope`].\r\n//!\r\n//! Split out from [`crate::frame`] (which owns the *envelope mechanics* and the\r\n//! version handshake) so the message *vocabulary* can grow additively without\r\n//! touching the wire-compat machinery. Every payload is a plain serde struct\r\n//! with serde's default unknown-field tolerance, so a newer brain may add a\r\n//! field and an older broker still decodes the message (KNOWN-HAZARDS 2.3).\r\n//!\r\n//! Bytes (PTY input/output) ride as base64 strings — JSON has no native byte\r\n//! type, and base64 keeps the single envelope/codec path rather than forking a\r\n//! binary frame format.\r\n// [impl->REQ-DAEMON-2]\r\n\r\nuse base64::engine::general_purpose::STANDARD as B64;\r\nuse base64::Engine;\r\nuse serde::{Deserialize, Serialize};\r\n\r\nuse spt_net::net::attach::AttachIntent;\r\n\r\nuse crate::effect::Minter;\r\nuse crate::frame::Envelope;\r\n\r\n/// Brain→broker: spawn a child under a fresh broker-owned PTY.\r\npub const KIND_SPAWN: &str = \"spawn\";\r\n/// Broker→brain: a spawn succeeded; carries the assigned session id + child pid.\r\npub const KIND_SPAWNED: &str = \"spawned\";\r\n/// Brain→broker: (re)subscribe to a session's output from a given sequence.\r\npub const KIND_SUBSCRIBE: &str = \"subscribe\";\r\n/// Brain→broker: release this connection's controller/viewer role on a session\r\n/// (W2.5, REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when\r\n/// the operator's viewport ends, so `driven_by` / `viewer_count` clear promptly\r\n/// even if the serving brain connection lives on (a connection drop also clears\r\n/// the role, so this is idempotent belt-and-braces).\r\npub const KIND_UNSUBSCRIBE: &str = \"unsubscribe\";\r\n/// Brain→broker: write raw bytes to a session's input (send-keys).\r\npub const KIND_INPUT: &str = \"input\";\r\n/// Brain→broker: resize a session's PTY.\r\npub const KIND_RESIZE: &str = \"resize\";\r\n/// Brain→broker: terminate a session's child.\r\npub const KIND_KILL: &str = \"kill\";\r\n/// CLI→broker: apply a live adapter update to every session running the adapter\r\n/// (W3d, ADR-0025). Per affected endpoint the broker stops the resident\r\n/// translation child (releasing the OS file lock), CRC-swaps the install dir from\r\n/// the CLI-extracted staging tree, then restarts the translation from the NEW\r\n/// on-disk manifest path. Replies [`KIND_APPLIED`] (ok) or [`KIND_ERROR`].\r\npub const KIND_ADAPTER_APPLY: &str = \"adapter-apply\";\r\n/// Broker→brain: a chunk of a session's output (pushed as the child produces it).\r\npub const KIND_OUTPUT: &str = \"output\";\r\n/// Broker→brain: a session's child exited.\r\npub const KIND_EXIT: &str = \"exit\";\r\n/// Broker→brain: the outcome of a [`KIND_SUBSCRIBE`] under the W2.5\r\n/// controller/viewer model (REQ-RCVIEW-1 / REQ-KICK-1). Carries the resolved\r\n/// role — controller / viewer / busy-refused / took-control — so the serving\r\n/// brain can wake (controller-only), stay wake-neutral (viewer), or refuse the\r\n/// operator (busy). A normal reply, not a new wire frame (the busy refusal is\r\n/// rendered client-side by `rc` from the endpoint's `driven_by`).\r\npub const KIND_SUBSCRIBED: &str = \"subscribed\";\r\n/// Broker→brain: the current PTY size of a session, pushed to a VIEWER on\r\n/// attach and to every viewer on a controller resize (REQ-RCVIEW-1). The\r\n/// serving brain forwards it as a [`spt_net::net::attach::AttachRecord::Size`]\r\n/// so the viewer letterboxes client-side (never resizing the PTY itself —\r\n/// resize is controller-exclusive).\r\npub const KIND_SIZE: &str = \"size\";\r\n/// Broker→brain: you (a controller) have been DISPLACED by a different operator's\r\n/// explicit `--take`/`kick` (REQ-KICK-1). The serving brain forwards this as a\r\n/// loud [`spt_net::net::attach::AttachRecord::Displaced`] on the wire, then\r\n/// detaches. Fired ONLY on a genuine cross-operator control-ownership change —\r\n/// NEVER on a same-origin re-serve after a brain restart (the gate-#7 self-kick\r\n/// trap: a successor re-taking the controller slot with the SAME `by` is silent).\r\npub const KIND_DISPLACED: &str = \"displaced\";\r\n/// Broker→brain(VIEWER): this viewer's bounded subscription queue overflowed and\r\n/// it was EVICTED (REQ-HAZARD-VIEWER-ISOLATION session-protection). DISTINCT from\r\n/// a session-exit EOF: the serving brain must NOT tear the attach down — it\r\n/// re-subscribes from the current ring floor (SKIP-TO-LIVE,\r\n/// REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). Written by the viewer's dedicated writer\r\n/// thread (OFF the log lock / off the drain) as its final frame.\r\npub const KIND_VIEWER_EVICTED: &str = \"viewer_evicted\";\r\n/// Broker→brain: an idempotent effect (an `input` carrying an `op_id`) is durably\r\n/// applied **or** was already applied (deduped). The exactly-once ack: a brain\r\n/// learns its effect landed, and a replay after a crash is acknowledged the same\r\n/// way so the brain can retire it (B5 / KNOWN-HAZARDS 7.2).\r\npub const KIND_APPLIED: &str = \"applied\";\r\n/// Broker→brain: a command failed (carries an operator-facing message).\r\npub const KIND_ERROR: &str = \"error\";\r\n/// Brain→broker: report the broker-owned net endpoint's status (D4a). The brain\r\n/// holds no endpoint of its own — the broker owns the node's network presence\r\n/// (ADR-0004 §B), so status (node id, dialable address, conn count) is an IPC ask.\r\npub const KIND_NET_STATUS: &str = \"net-status\";\r\n/// Broker→brain: the net endpoint status reply.\r\npub const KIND_NET_STATUS_REPLY: &str = \"net-status-reply\";\r\n/// Brain→broker: report the broker's OWN compiled image version (F-025). The\r\n/// broker SURVIVES `spt update apply` (only the brain restarts — ADR-0018 D3-3),\r\n/// so a freshly-applied node may still be RUNNING an older broker binary; the\r\n/// broker is the authoritative source for \"what version is actually executing\",\r\n/// answered from its own compile-time constant — NEVER inferred from disk. An\r\n/// older broker that predates this kind replies `unknown command kind` (the\r\n/// router's forward-compat default), which the CLI reads as \"too old to report\".\r\npub const KIND_BROKER_IMAGE: &str = \"broker-image\";\r\n/// Broker→brain: the broker image version reply.\r\npub const KIND_BROKER_IMAGE_REPLY: &str = \"broker-image-reply\";\r\n/// Brain→broker query: the brain-subscriber stall-evict tally (LIFECYCLE-TRUTH W2\r\n/// observability rider, REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE) — how many\r\n/// controllers were stall-evicted for a suspended/black-holed brain conn since\r\n/// boot, and when the most recent fired. Surfaced on `daemon status` so a brain\r\n/// that never returns is a diagnosable fact. An older broker predating this kind\r\n/// replies `unknown command kind` (forward-compat), read as \"not reported\".\r\npub const KIND_STALL_EVICTS: &str = \"stall-evicts\";\r\n/// Broker→brain: the stall-evict tally reply.\r\npub const KIND_STALL_EVICTS_REPLY: &str = \"stall-evicts-reply\";\r\n/// Brain→broker: dial a peer over the broker-owned endpoint (D4a). With an\r\n/// `op_id` the dial is exactly-once (journaled like an idempotent `input`).\r\npub const KIND_NET_DIAL: &str = \"net-dial\";\r\n/// Brain→broker: mint (or reuse) the broker's single in-process loopback\r\n/// connection (M12 W1.5, B1) — the transport a LOCAL `spt rc` rides because QUIC\r\n/// self-dial is refused by iroh. An explicit verb (NOT a sentinel addr on\r\n/// [`KIND_NET_DIAL`]): loopback handling stays isolated from the dial hot path.\r\n/// Null payload (the singleton is idempotent by reuse — no op id, no addr).\r\n/// Answered with a [`KIND_NET_DIALED`] whose `remote_id_hex` is THIS node's own id.\r\npub const KIND_NET_DIAL_LOOPBACK: &str = \"net-dial-loopback\";\r\n/// Broker→brain: a dial durably landed — fresh or deduped (`applied_now`\r\n/// distinguishes, mirroring [`KIND_APPLIED`]). Carries the broker-minted conn id.\r\npub const KIND_NET_DIALED: &str = \"net-dialed\";\r\n/// Brain→broker: open a bidi QUIC stream on a held connection (D4b). With an\r\n/// `op_id` the open is exactly-once (journaled like a dial).\r\npub const KIND_NET_STREAM_OPEN: &str = \"net-stream-open\";\r\n/// Broker→brain: a stream open durably landed (fresh or deduped).\r\npub const KIND_NET_STREAM_OPENED: &str = \"net-stream-opened\";\r\n/// Brain→broker: write bytes to a held stream's send side (D4b). With an\r\n/// `op_id` the write is exactly-once (journaled; acked with [`KIND_NET_SENT`]).\r\npub const KIND_NET_STREAM_SEND: &str = \"net-stream-send\";\r\n/// Broker→brain: a journaled stream send durably landed (fresh or deduped).\r\npub const KIND_NET_SENT: &str = \"net-sent\";\r\n/// Brain→broker: list the streams the broker currently holds (D4b). The query\r\n/// model a re-attaching brain uses to find streams that arrived while it was\r\n/// down — no announce buffer to miss.\r\npub const KIND_NET_STREAMS: &str = \"net-streams\";\r\n/// Broker→brain: the held-streams reply.\r\npub const KIND_NET_STREAMS_REPLY: &str = \"net-streams-reply\";\r\n/// Brain→broker: (re)subscribe to a stream's buffered reads from a cursor —\r\n/// the PTY `subscribe` discipline applied to a QUIC stream (gapless resume).\r\npub const KIND_NET_STREAM_SUBSCRIBE: &str = \"net-stream-subscribe\";\r\n/// Broker→brain: a sequenced chunk of a stream's reads (live or replayed).\r\npub const KIND_NET_STREAM_DATA: &str = \"net-stream-data\";\r\n/// Broker→brain: a stream's read side finished cleanly; `next_seq` tells the\r\n/// brain the final cursor so it can verify it missed nothing.\r\npub const KIND_NET_STREAM_EOF: &str = \"net-stream-eof\";\r\n/// Brain→broker: (re)subscribe to the broker's conn-liveness event log from a\r\n/// cursor (D4c) — the PresenceChannel broker seam. Same gapless-resume\r\n/// discipline as a stream subscribe.\r\n// [impl->REQ-EP-4]\r\npub const KIND_NET_PRESENCE_SUBSCRIBE: &str = \"net-presence-subscribe\";\r\n/// Broker→brain: one sequenced conn-liveness event (live or replayed).\r\npub const KIND_NET_PRESENCE_EVENT: &str = \"net-presence-event\";\r\n/// Brain→broker (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION): submit a NON-BLOCKING\r\n/// dial. Payload = the existing [`NetDialReq`] (addr + op_id + minter). The\r\n/// broker acks immediately ([`KIND_NET_DIAL_SUBMITTED`]) and dials CONCURRENTLY\r\n/// on its runtime under W1's per-dial bound; the OUTCOME rides the presence\r\n/// stream ([`PRESENCE_CONNECTED`] on success, [`PRESENCE_DIAL_FAILED`] on\r\n/// failure) — never a blocking reply. Contrast [`KIND_NET_DIAL`] (blocking\r\n/// request-reply, kept for its other callers: shellwake, pairing, rc).\r\n// [impl->REQ-PUMP-PEER-ISOLATION]\r\npub const KIND_NET_DIAL_SUBMIT: &str = \"net-dial-submit\";\r\n/// Broker→brain (PUMP-TRUTH W2): the immediate ack that a [`KIND_NET_DIAL_SUBMIT`]\r\n/// was accepted and the concurrent dial spawned. Bare ack (null payload) — the\r\n/// dial's real outcome rides the presence stream, not this frame.\r\n// [impl->REQ-PUMP-PEER-ISOLATION]\r\npub const KIND_NET_DIAL_SUBMITTED: &str = \"net-dial-submitted\";\r\n\r\n/// [`NetPresenceEvent::kind`]: a connection joined the broker's conn table.\r\npub const PRESENCE_CONNECTED: &str = \"connected\";\r\n/// [`NetPresenceEvent::kind`]: a connection ended and left the conn table.\r\npub const PRESENCE_DISCONNECTED: &str = \"disconnected\";\r\n/// [`NetPresenceEvent::kind`] (PUMP-TRUTH W2, REQ-PUMP-PEER-ISOLATION): a\r\n/// NON-BLOCKING dial submitted via [`KIND_NET_DIAL_SUBMIT`] failed to connect.\r\n/// The outcome-channel for a fire-and-forget dial (its success rides\r\n/// [`PRESENCE_CONNECTED`] as today). Carries no live connection → `conn_id: 0`\r\n/// sentinel; consumers correlate a fail by `kind` + `remote_id_hex`, NEVER\r\n/// `conn_id`. Additive kind — an older brain tolerates it (the `kind` string is\r\n/// forward-compat by design, 2.3).\r\n// [impl->REQ-PUMP-PEER-ISOLATION]\r\npub const PRESENCE_DIAL_FAILED: &str = \"dial-failed\";\r\n\r\n/// Brain→broker: run the joiner leg of the subnet pairing ceremony (M7 D3,\r\n/// REQ-SUBNET-2). The ceremony must run broker-side: it dials via the\r\n/// rendezvous and binds/pins the broker-owned endpoint's REAL node pubkey.\r\npub const KIND_PAIR_JOIN: &str = \"pair-join\";\r\n/// Broker→brain: the join ceremony's outcome (success or classified failure).\r\npub const KIND_PAIR_JOINED: &str = \"pair-joined\";\r\n\r\n/// Brain→broker: phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE) — MEET a\r\n/// seed-holder for the named subnet (no code). The daemon resolves and holds the\r\n/// member's real pairing address, replying [`KIND_MET_MEMBER`].\r\npub const KIND_PAIR_MEET: &str = \"pair-meet\";\r\n/// Broker→brain: a seed-holder was met — its real address is held under a\r\n/// session id, awaiting the code ([`KIND_PAIR_CODE_SUBMIT`]).\r\npub const KIND_MET_MEMBER: &str = \"met-member\";\r\n/// Brain→broker: phase 2 of the two-phase join — submit the code for a held\r\n/// meet session. The daemon dials the HELD address (no re-search) and runs the\r\n/// SPAKE2 ceremony, replying [`KIND_PAIR_JOINED`]. A wrong-code retry re-submits\r\n/// against the same session id (ceremony only).\r\npub const KIND_PAIR_CODE_SUBMIT: &str = \"pair-code-submit\";\r\n\r\n/// Brain→broker: list hosted sessions (id + owning endpoint). The inbound\r\n/// dispatcher's session→endpoint resolution (D9-1).\r\npub const KIND_SESSIONS: &str = \"sessions\";\r\n/// Broker→brain reply to [`KIND_SESSIONS`].\r\npub const KIND_SESSIONS_REPLY: &str = \"sessions-reply\";\r\n\r\n/// Client→broker: deliver an inbound message to an spt-hosted endpoint by\r\n/// **endpoint id** — the broker resolves the endpoint to its hosted session and\r\n/// injects the bytes straight into that session's PTY (REQ-SEND-SPT-HOSTED,\r\n/// CONTEXT:39/187-188 \"the daemon injects events straight into the PTY\"). The\r\n/// endpoint-keyed counterpart of [`KIND_INPUT`] (which addresses a session by the\r\n/// broker-minted u64 a CLI can't know): atomic endpoint→session resolution under\r\n/// the broker's lock, one round-trip, no TOCTOU. Fire-and-forget (no `op_id`,\r\n/// no journal — a user message is delivered like the relay's `<EVENT>` emit, not\r\n/// an exactly-once effect).\r\npub const KIND_ENDPOINT_INPUT: &str = \"endpoint-input\";\r\n/// Broker→client reply to [`KIND_ENDPOINT_INPUT`]: whether a hosted session for\r\n/// the endpoint was found and the bytes were written to its PTY. `delivered=false`\r\n/// (no matching broker session) tells the caller to fall back to the spool.\r\npub const KIND_ENDPOINT_INJECTED: &str = \"endpoint-injected\";\r\n\r\n/// Client→broker: trigger a **planned brain-process restart** (ADR-0018 D3-3,\r\n/// Q3). `spt update apply` sends this after swapping the binary on disk; the\r\n/// broker signals its brain supervisor to cycle the brain child with\r\n/// `start-reason=update`, so the live brain restarts onto the new binary while\r\n/// every broker-held endpoint stays untouched at the process level. Additive\r\n/// verb (KH-2.3): an older broker that predates it answers with a generic error,\r\n/// which the caller surfaces — it never silently no-ops a swapped binary.\r\npub const KIND_BRAIN_RESTART: &str = \"brain-restart\";\r\n/// Broker→client ack for [`KIND_BRAIN_RESTART`]: the restart was *accepted*\r\n/// (the supervisor was signalled). Readiness/rollback gating is D6 — this ack is\r\n/// acceptance, not completion. `honored` is false when the broker has no\r\n/// supervisor wired (e.g. a bare test broker), so the caller knows the trigger\r\n/// reached a broker that cannot itself respawn.\r\npub const KIND_BRAIN_RESTARTED: &str = \"brain-restarted\";\r\n\r\n/// `spawn` payload — the program + args + initial geometry. The broker spawns\r\n/// it under a PTY (no shell interpretation; argv is passed through).\r\n#[derive(Debug, Clone, Serialize, Deserialize)]\r\npub struct SpawnReq {\r\n    /// The program to exec (resolved on `PATH` by the OS spawn).\r\n    pub program: String,\r\n    /// Its arguments, verbatim.\r\n    #[serde(default)]\r\n    pub args: Vec<String>,\r\n    /// Initial PTY rows.\r\n    pub rows: u16,\r\n    /// Initial PTY cols.\r\n    pub cols: u16,\r\n    /// The endpoint this session belongs to (D9-1: the remote-drive\r\n    /// dispatcher's gate subject — `serve_attach` gates on the *owning*\r\n    /// endpoint's whitelist, resolved from the broker's table, never from\r\n    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher\r\n    /// refuses to serve an unlabeled session, fail-closed.\r\n    #[serde(default)]\r\n    pub endpoint: String,\r\n    /// The working directory to spawn the PTY child in (REQ-HOST-RUN-2). `None`\r\n    /// inherits the broker's cwd — the legacy/test behavior and the W1 bringup\r\n    /// shortcut. `Some(dir)` lands the child in a PROJECT-scoped cwd: the\r\n    /// consumer (Claude Code) reads its `.claude`, session history, and digest\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,301]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\msg.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1478,"totalBytes":16674,"outputLines":263,"outputBytes":16674,"shownRange":{"start":1,"end":263},"nextOffset":264}}},"isError":false,"timestamp":1784197455183}}
{"type":"message","id":"ccca4cae","parentId":"d40b9ff3","timestamp":"2026-07-16T10:24:15.201Z","message":{"role":"toolResult","toolCallId":"call_pvneLNsgNnfURSwNTznYUjWQ|fc_017bff223290b7ba016a58b14f1d68819aa284cddd39f8b18c","toolName":"read","content":[{"type":"text","text":"//! Per-subnet eventually-consistent endpoint registry (D3a/b/d/e, ADR-0006).\n//!\n//! Maps `endpoint_id → [instance...]` within one subnet, where an [`Instance`] is the\n//! same logical endpoint as seen running on a particular node, with a liveness\n//! [`Status`] and the author node's monotonic [`epoch`](Instance::epoch). The same\n//! `endpoint_id` can hold several instances (the node-global mind advertised from more\n//! than one machine); picking the right one is the resolution policy's job (D3c) — this\n//! module owns the **data model + the merge lease**, not resolution.\n//!\n//! ## The epoch lease (#8)\n//!\n//! Registry updates arrive out of order across a partition or under clock skew. A\n//! lagging node could re-announce a stale `Active` after the endpoint has actually gone\n//! `Offline`. [`merge_instance`] defends against this with a **version-vector** rule\n//! keyed on `(endpoint_id, node)`: an incoming update wins iff its epoch is **strictly\n//! greater** than the stored one for that node. An equal or lower epoch is dropped as\n//! stale — so a newer `Offline` (higher epoch) can never be clobbered by a lagging\n//! `Active` (lower epoch). The epoch is the per-node monotonic counter\n//! ([`spt_store::epoch`]), NEVER wall-clock. Cross-node *replication* of these updates\n//! rides the transport (D4); the merge seam is identical for local and wire-delivered\n//! updates.\n//!\n//! ## Join-time bare-id collision check (D3d)\n//!\n//! Bare-id uniqueness holds *within* a subnet (ADR-0006 §1): advertising a joining\n//! endpoint whose id another node already holds in the target subnet refuses via\n//! [`join_endpoint`](SubnetRegistry::join_endpoint) / [`JoinCollision`] — the clash is\n//! surfaced and resolved (rename, D3f) before advertisement. Per-subnet: the same bare\n//! name as *distinct* endpoints in different subnets is legal; the resolver forces\n//! qualification (D3c).\n//!\n//! ## Visibility enforcement (D3e)\n//!\n//! Excluded = **not advertised AND not routable** (ADR-0006 §6). The policy lives in\n//! [`spt_store::visibility::VisibilityStore`]; this module enforces it at both\n//! registry boundaries: [`advertise_if_visible`] keeps hidden endpoints out of the\n//! registry entirely, and [`resolve_visible`](SubnetRegistry::resolve_visible) /\n//! [`resolve_across_visible`] refuse to route to them (even via an explicit `@node`\n//! pin). Exclusion removes a subnet from the cross-subnet ambiguity set — hidden in\n//! one subnet, visible in another resolves cleanly in the visible one.\n// [impl->REQ-INST-7]\n\nuse std::collections::BTreeMap;\n\nuse serde::{Deserialize, Serialize};\nuse spt_proto::addr::Address;\nuse spt_store::visibility::VisibilityStore;\n\n/// An endpoint instance's liveness within a subnet. Ordering carries no precedence —\n/// precedence is the epoch's job; status is just the advertised state.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\npub enum Status {\n    /// Live and currently reachable.\n    Active,\n    /// Known but idle (e.g. a paused live agent) — addressable, not currently serving.\n    Dormant,\n    /// Resting **cold** (D9-2, REQ-INST-3): the harness session is closed,\n    /// resumed-on-wake. Still addressable (`spt wake ling@desktop` must route) —\n    /// only `Offline` is unroutable. Additive variant on a pre-release fleet;\n    /// the NDJSON decode posture (unknown-kind skip) is the registry record's\n    /// forward-compat story.\n    // [impl->REQ-INST-3]\n    Suspended,\n    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active\n    /// suppression can be reasoned about; the resolver skips it.\n    Offline,\n}\n\n/// One endpoint as seen on one node: the node it runs on, its advertised [`Status`], and\n/// the author node's monotonic [`epoch`](Self::epoch) (the merge-precedence key).\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct Instance {\n    /// The node this instance runs on (node-qualifier in `id@node` addressing).\n    pub node: String,\n    /// Advertised liveness.\n    pub status: Status,\n    /// The author node's per-node monotonic epoch at the time of this update. The sole\n    /// precedence key in [`merge_instance`] — strictly-greater wins.\n    pub epoch: u64,\n    /// The resource-advertisement blurb (D9-4, REQ-INST-14 — CONTEXT §resource\n    /// advertisement): a free-text \"yellow-pages\" line describing what this\n    /// endpoint can serve. **Not a separate registry** — a field on the\n    /// instance row, riding the existing epoch lease + `RegistryUpdate`\n    /// replication (a blurb update is ordered like any instance update);\n    /// the \"subnet resource registry\" is a [projection](resource_projection)\n    /// of these rows. Additive serde-default: absent = no blurb, and a\n    /// pre-D9-4 record parses clean.\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub resources: Option<String>,\n    /// The presence datum (M5-D6a, REQ-PRES-1 — CONTEXT §presence): this\n    /// instance's `last_active_ms` recency stamp as of the authoring node's\n    /// advertisement scan. **Not a separate gossip channel** — a field on the\n    /// instance row riding the existing epoch lease + `RegistryUpdate`\n    /// replication, exactly like [`resources`](Self::resources); subnet-wide\n    /// `(last_active_node, last_active_endpoint, ts)` is a projection of\n    /// these rows. Wall-clock ms, cross-compared ONLY as a routing heuristic\n    /// (most-recently-active resolution) — the epoch stays the sole merge-\n    /// precedence key; clock skew can cost a suboptimal surface target, never\n    /// a wrong merge. Additive serde-default: a pre-D6 row parses clean.\n    // [impl->REQ-PRES-1]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub last_active_ms: Option<u64>,\n    /// The authoring node's subnet-broadcast shell adapters (M5-D8c — CONTEXT\n    /// §Shell model \"Discovery scope\": shell adapters on *other* subnet nodes\n    /// with `broadcast = subnet` surface to agents). **Not a shell-instance\n    /// registry** (shell perches stay private to the owner link, CONTEXT\n    /// \"Not in the subnet registry\") — the names of `kind=\"shell\"` adapters\n    /// REGISTERED on the authoring node that chose subnet-wide broadcast,\n    /// riding the instance row like [`resources`](Self::resources) does\n    /// (same epoch lease, same visibility gate, no new channel; node-level\n    /// data carried per-row because the rows are the distribution).\n    /// Discovery only: spawning one rides the deferred instantiate-anywhere\n    /// gate. Additive serde-default: a pre-D8 row parses clean.\n    // [impl->REQ-EP-5]\n    #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n    pub shell_adapters: Vec<String>,\n    /// The authoring node's human-readable label (M7 D2, REQ-SUBNET-3 —\n    /// CONTEXT §node label): defaults to the node's OS hostname, re-checked\n    /// at daemon startup (a hostname change updates the advertised label).\n    /// Display + addressing sugar only — the pubkey stays the identity; views\n    /// render `LABEL (keyprefix…)`. **Not a separate channel** — node-level\n    /// data riding the instance row like [`resources`](Self::resources) does\n    /// (the rows are the distribution; same epoch lease, same visibility\n    /// gate). Additive serde-default: a pre-M7 row parses clean.\n    // [impl->REQ-SUBNET-3]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub node_label: Option<String>,\n    /// The authoring node's stable machine identifier, **hashed** (M8\n    /// decision 13/22, REQ-SUBNET-7): domain-separated SHA-256 of the OS\n    /// machine id (`/etc/machine-id` / Windows `MachineGuid`; spt-minted\n    /// persisted UUID where unreadable) — never the raw id on the wire.\n    /// The re-pair overwrite anchor: a completed pairing ceremony presenting\n    /// the same node label AND the same machine id as an existing trusted\n    /// row marks that row's identity as superseded (the machine re-paired\n    /// under a new key). Node-level data riding the instance row like\n    /// [`node_label`](Self::node_label) (the rows are the distribution;\n    /// same epoch lease, same visibility gate). Additive serde-default: a\n    /// pre-M8 row parses clean.\n    // [impl->REQ-SUBNET-7]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub machine_id: Option<String>,\n    /// The endpoint's advertised **type tag** (M9, REQ-MSG-6 — CONTEXT\n    /// §Gateway): the open-type tag this endpoint binds under\n    /// ([`spt_proto::endpoint::EndpointType::as_tag`]; `\"gateway\"` for a\n    /// Gateway surface). The cross-node `user-msg` identity gate reads this:\n    /// `receive_wan` resolves the origin endpoint's advertised type, keyed by\n    /// the QUIC-proven origin node, and honors a `user-msg` only from a\n    /// Gateway-typed origin (REQ-MSG-5 over the WAN). A node advertises only\n    /// its **own** endpoints' types (no cross-node authorship); the field\n    /// extends REQ-INST-7's data model and rides the existing epoch lease +\n    /// `RegistryUpdate` replication like [`resources`](Self::resources) —\n    /// no new channel, no new merge key. Additive serde-default: an N-1 row\n    /// (no field) parses clean and resolves to \"not provably user-backed\"\n    /// (the fail-closed re-stamp; rollout grace).\n    // [impl->REQ-MSG-6]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub endpoint_type: Option<String>,\n    /// Whether this instance is **bound** (a bound live broker session) vs\n    /// **unbound** (a live broker session that has not bound yet — pre-bind,\n    /// rc-attachable but not message-addressable). The unified subnet-display\n    /// parity datum (REQ-SUBNET-DISPLAY-PARITY): a remote viewer renders the\n    /// bound/unbound fill exactly as the home node does, instead of reducing\n    /// every remote row to plain green. `true` ⇒ bound; `false` ⇒ unbound (or a\n    /// cold/suspended perch — `bound=false` there is harmless, the picker keys\n    /// the unbound *display* on `Status::Active|Dormant && !bound`). Node-level\n    /// data riding the instance row like [`endpoint_type`](Self::endpoint_type)\n    /// — same epoch lease, same visibility gate, no new channel. Additive\n    /// serde-default: an N-1 row (no field) parses clean and defaults to `true`\n    /// (bound), the conservative no-special-display posture (a row that omits\n    /// the field renders as a plain bound endpoint, never a phantom unbound).\n    // [impl->REQ-SUBNET-DISPLAY-PARITY]\n    #[serde(default = \"default_bound\")]\n    pub bound: bool,\n    /// The node currently **controlling** this instance's session (its\n    /// broker-owned `driven_by`), if any — the driver node hex. Threaded so a\n    /// remote viewer renders the blue \"controlled\" state + the desc-pane\n    /// `controlled by <node>` pin for an endpoint another node is driving, not\n    /// just the local node's own perches (REQ-SUBNET-DISPLAY-PARITY). Node-level\n    /// data riding the instance row; additive serde-default: an N-1 row parses\n    /// clean to `None` (no controller — the existing remote behaviour).\n    // [impl->REQ-SUBNET-DISPLAY-PARITY]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub controller_node: Option<String>,\n    /// Whether this instance is **harness-only** — a LiveAgent with no broker\n    /// PTY seat (harness-hosted, not rc-controllable). Threaded so a remote\n    /// viewer renders the amber-hollow \"harness-only\" state instead of plain\n    /// green (REQ-SUBNET-DISPLAY-PARITY); mirrors the home node's\n    /// `controllable == Some(false)` derivation for a `live_agent`. Node-level\n    /// data riding the instance row; additive serde-default: an N-1 row parses\n    /// clean to `false` (not harness-only — the conservative posture).\n    // [impl->REQ-SUBNET-DISPLAY-PARITY]\n    #[serde(default, skip_serializing_if = \"is_false\")]\n    pub harness_only: bool,\n    /// The endpoint's advertised harness **adapter** (composite `<adapter>[:profile]`,\n    /// [`spt_store::info::Info::adapter`]) — gossiped so a remote picker's desc pane\n    /// shows the real harness instead of the resources *blurb* masquerading as one\n    /// (#4, REQ-GOSSIP-ADAPTER-PROJECTS: `from_resource_row` used to pass the blurb as\n    /// the adapter). Node-level data riding the instance row like\n    /// [`endpoint_type`](Self::endpoint_type) — same epoch lease, same visibility gate.\n    /// Additive serde-default: an N-1 row parses clean to `None` (the picker renders `-`).\n    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub adapter: Option<String>,\n    /// The endpoint's **recent projects**, newest-first, project IDs ONLY\n    /// (owlery-internal excluded, bounded) — gossiped so a remote picker/list renders\n    /// real project history instead of the hardcoded empty `from_resource_row` used to\n    /// fake (#4). Project IDs only: dirs leak usernames/dir structure across nodes, so\n    /// they never cross the wire (the local Choose-project step keeps its own dirs).\n    /// Rides the instance row; additive serde-default: an N-1 row parses clean to empty.\n    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n    #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\n    pub recent_projects: Vec<String>,\n    /// Whether ANY operator (local OR remote) currently drives this instance — the\n    /// broker-owned any-controller marker ([`spt_store::info::Info::controlled`]).\n    /// Gossiped **explicitly**, NOT derived from [`controller_node`](Self::controller_node):\n    /// a LOCALLY-controlled endpoint has a remote-only `driven_by=None` (KH 7.15), so a\n    /// remote viewer reading controlled-ness off the WHO datum would undercount the\n    /// local-controller case (`controlled=true` yet no gossiped driver) and render it\n    /// plain ONLINE — reopening the #3 hole cross-node (doyle ruling). This carries the\n    /// raw truth; `controller_node` stays the separate WHO datum (may be `None` while\n    /// `controlled=true`). Additive serde-default: an N-1 row parses clean to `false`.\n    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n    #[serde(default, skip_serializing_if = \"is_false\")]\n    pub controlled: bool,\n}\n\n/// The serde default for [`Instance::bound`]: an N-1 row (authored before the\n/// field existed) is treated as **bound** — the conservative posture (a\n/// field-less row renders as a plain bound endpoint, never a phantom unbound).\n// [impl->REQ-SUBNET-DISPLAY-PARITY]\nfn default_bound() -> bool {\n    true\n}\n\n/// `skip_serializing_if` for the `false` (default) case of\n/// [`Instance::harness_only`] — keeps the wire compact (the common case omits\n/// the field, exactly as an N-1 row does).\n#[allow(clippy::trivially_copy_pass_by_ref)]\nfn is_false(b: &bool) -> bool {\n    !*b\n}\n\n/// One node-level label datum (REQ-SUBNET-3 endpoint-less gap): a node's\n/// advertised hostname, carried INDEPENDENT of endpoint rows so a node with\n/// **zero endpoints** still names itself to peers (the per-row\n/// [`Instance::node_label`] only rides endpoint rows, so an endpoint-less node\n/// otherwise renders a bare key-prefix). Merged under the same\n/// strictly-greater-epoch lease as instance rows; evicted with the node.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct NodeLabel {\n    /// The node's advertised label (its OS hostname).\n    pub label: String,\n    /// The lease epoch (strictly-greater supersedes — the instance-row rule).\n    pub epoch: u64,\n}\n\n/// The per-subnet registry: `endpoint_id → [instance...]`. `BTreeMap`/`Vec` for stable,\n/// deterministic serde ordering (no hash-seed nondeterminism across nodes).\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\npub struct SubnetRegistry {\n    #[serde(default)]\n    by_id: BTreeMap<String, Vec<Instance>>,\n    /// Node-level labels (`node_hex → label`), the endpoint-less carrier\n…\n}\n\n[Showing lines 1-263 of 2572. Use :264 to continue]"}],"details":{"truncation":{"content":"//! Per-subnet eventually-consistent endpoint registry (D3a/b/d/e, ADR-0006).\r\n//!\r\n//! Maps `endpoint_id → [instance...]` within one subnet, where an [`Instance`] is the\r\n//! same logical endpoint as seen running on a particular node, with a liveness\r\n//! [`Status`] and the author node's monotonic [`epoch`](Instance::epoch). The same\r\n//! `endpoint_id` can hold several instances (the node-global mind advertised from more\r\n//! than one machine); picking the right one is the resolution policy's job (D3c) — this\r\n//! module owns the **data model + the merge lease**, not resolution.\r\n//!\r\n//! ## The epoch lease (#8)\r\n//!\r\n//! Registry updates arrive out of order across a partition or under clock skew. A\r\n//! lagging node could re-announce a stale `Active` after the endpoint has actually gone\r\n//! `Offline`. [`merge_instance`] defends against this with a **version-vector** rule\r\n//! keyed on `(endpoint_id, node)`: an incoming update wins iff its epoch is **strictly\r\n//! greater** than the stored one for that node. An equal or lower epoch is dropped as\r\n//! stale — so a newer `Offline` (higher epoch) can never be clobbered by a lagging\r\n//! `Active` (lower epoch). The epoch is the per-node monotonic counter\r\n//! ([`spt_store::epoch`]), NEVER wall-clock. Cross-node *replication* of these updates\r\n//! rides the transport (D4); the merge seam is identical for local and wire-delivered\r\n//! updates.\r\n//!\r\n//! ## Join-time bare-id collision check (D3d)\r\n//!\r\n//! Bare-id uniqueness holds *within* a subnet (ADR-0006 §1): advertising a joining\r\n//! endpoint whose id another node already holds in the target subnet refuses via\r\n//! [`join_endpoint`](SubnetRegistry::join_endpoint) / [`JoinCollision`] — the clash is\r\n//! surfaced and resolved (rename, D3f) before advertisement. Per-subnet: the same bare\r\n//! name as *distinct* endpoints in different subnets is legal; the resolver forces\r\n//! qualification (D3c).\r\n//!\r\n//! ## Visibility enforcement (D3e)\r\n//!\r\n//! Excluded = **not advertised AND not routable** (ADR-0006 §6). The policy lives in\r\n//! [`spt_store::visibility::VisibilityStore`]; this module enforces it at both\r\n//! registry boundaries: [`advertise_if_visible`] keeps hidden endpoints out of the\r\n//! registry entirely, and [`resolve_visible`](SubnetRegistry::resolve_visible) /\r\n//! [`resolve_across_visible`] refuse to route to them (even via an explicit `@node`\r\n//! pin). Exclusion removes a subnet from the cross-subnet ambiguity set — hidden in\r\n//! one subnet, visible in another resolves cleanly in the visible one.\r\n// [impl->REQ-INST-7]\r\n\r\nuse std::collections::BTreeMap;\r\n\r\nuse serde::{Deserialize, Serialize};\r\nuse spt_proto::addr::Address;\r\nuse spt_store::visibility::VisibilityStore;\r\n\r\n/// An endpoint instance's liveness within a subnet. Ordering carries no precedence —\r\n/// precedence is the epoch's job; status is just the advertised state.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\r\npub enum Status {\r\n    /// Live and currently reachable.\r\n    Active,\r\n    /// Known but idle (e.g. a paused live agent) — addressable, not currently serving.\r\n    Dormant,\r\n    /// Resting **cold** (D9-2, REQ-INST-3): the harness session is closed,\r\n    /// resumed-on-wake. Still addressable (`spt wake ling@desktop` must route) —\r\n    /// only `Offline` is unroutable. Additive variant on a pre-release fleet;\r\n    /// the NDJSON decode posture (unknown-kind skip) is the registry record's\r\n    /// forward-compat story.\r\n    // [impl->REQ-INST-3]\r\n    Suspended,\r\n    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active\r\n    /// suppression can be reasoned about; the resolver skips it.\r\n    Offline,\r\n}\r\n\r\n/// One endpoint as seen on one node: the node it runs on, its advertised [`Status`], and\r\n/// the author node's monotonic [`epoch`](Self::epoch) (the merge-precedence key).\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct Instance {\r\n    /// The node this instance runs on (node-qualifier in `id@node` addressing).\r\n    pub node: String,\r\n    /// Advertised liveness.\r\n    pub status: Status,\r\n    /// The author node's per-node monotonic epoch at the time of this update. The sole\r\n    /// precedence key in [`merge_instance`] — strictly-greater wins.\r\n    pub epoch: u64,\r\n    /// The resource-advertisement blurb (D9-4, REQ-INST-14 — CONTEXT §resource\r\n    /// advertisement): a free-text \"yellow-pages\" line describing what this\r\n    /// endpoint can serve. **Not a separate registry** — a field on the\r\n    /// instance row, riding the existing epoch lease + `RegistryUpdate`\r\n    /// replication (a blurb update is ordered like any instance update);\r\n    /// the \"subnet resource registry\" is a [projection](resource_projection)\r\n    /// of these rows. Additive serde-default: absent = no blurb, and a\r\n    /// pre-D9-4 record parses clean.\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub resources: Option<String>,\r\n    /// The presence datum (M5-D6a, REQ-PRES-1 — CONTEXT §presence): this\r\n    /// instance's `last_active_ms` recency stamp as of the authoring node's\r\n    /// advertisement scan. **Not a separate gossip channel** — a field on the\r\n    /// instance row riding the existing epoch lease + `RegistryUpdate`\r\n    /// replication, exactly like [`resources`](Self::resources); subnet-wide\r\n    /// `(last_active_node, last_active_endpoint, ts)` is a projection of\r\n    /// these rows. Wall-clock ms, cross-compared ONLY as a routing heuristic\r\n    /// (most-recently-active resolution) — the epoch stays the sole merge-\r\n    /// precedence key; clock skew can cost a suboptimal surface target, never\r\n    /// a wrong merge. Additive serde-default: a pre-D6 row parses clean.\r\n    // [impl->REQ-PRES-1]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub last_active_ms: Option<u64>,\r\n    /// The authoring node's subnet-broadcast shell adapters (M5-D8c — CONTEXT\r\n    /// §Shell model \"Discovery scope\": shell adapters on *other* subnet nodes\r\n    /// with `broadcast = subnet` surface to agents). **Not a shell-instance\r\n    /// registry** (shell perches stay private to the owner link, CONTEXT\r\n    /// \"Not in the subnet registry\") — the names of `kind=\"shell\"` adapters\r\n    /// REGISTERED on the authoring node that chose subnet-wide broadcast,\r\n    /// riding the instance row like [`resources`](Self::resources) does\r\n    /// (same epoch lease, same visibility gate, no new channel; node-level\r\n    /// data carried per-row because the rows are the distribution).\r\n    /// Discovery only: spawning one rides the deferred instantiate-anywhere\r\n    /// gate. Additive serde-default: a pre-D8 row parses clean.\r\n    // [impl->REQ-EP-5]\r\n    #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\r\n    pub shell_adapters: Vec<String>,\r\n    /// The authoring node's human-readable label (M7 D2, REQ-SUBNET-3 —\r\n    /// CONTEXT §node label): defaults to the node's OS hostname, re-checked\r\n    /// at daemon startup (a hostname change updates the advertised label).\r\n    /// Display + addressing sugar only — the pubkey stays the identity; views\r\n    /// render `LABEL (keyprefix…)`. **Not a separate channel** — node-level\r\n    /// data riding the instance row like [`resources`](Self::resources) does\r\n    /// (the rows are the distribution; same epoch lease, same visibility\r\n    /// gate). Additive serde-default: a pre-M7 row parses clean.\r\n    // [impl->REQ-SUBNET-3]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub node_label: Option<String>,\r\n    /// The authoring node's stable machine identifier, **hashed** (M8\r\n    /// decision 13/22, REQ-SUBNET-7): domain-separated SHA-256 of the OS\r\n    /// machine id (`/etc/machine-id` / Windows `MachineGuid`; spt-minted\r\n    /// persisted UUID where unreadable) — never the raw id on the wire.\r\n    /// The re-pair overwrite anchor: a completed pairing ceremony presenting\r\n    /// the same node label AND the same machine id as an existing trusted\r\n    /// row marks that row's identity as superseded (the machine re-paired\r\n    /// under a new key). Node-level data riding the instance row like\r\n    /// [`node_label`](Self::node_label) (the rows are the distribution;\r\n    /// same epoch lease, same visibility gate). Additive serde-default: a\r\n    /// pre-M8 row parses clean.\r\n    // [impl->REQ-SUBNET-7]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub machine_id: Option<String>,\r\n    /// The endpoint's advertised **type tag** (M9, REQ-MSG-6 — CONTEXT\r\n    /// §Gateway): the open-type tag this endpoint binds under\r\n    /// ([`spt_proto::endpoint::EndpointType::as_tag`]; `\"gateway\"` for a\r\n    /// Gateway surface). The cross-node `user-msg` identity gate reads this:\r\n    /// `receive_wan` resolves the origin endpoint's advertised type, keyed by\r\n    /// the QUIC-proven origin node, and honors a `user-msg` only from a\r\n    /// Gateway-typed origin (REQ-MSG-5 over the WAN). A node advertises only\r\n    /// its **own** endpoints' types (no cross-node authorship); the field\r\n    /// extends REQ-INST-7's data model and rides the existing epoch lease +\r\n    /// `RegistryUpdate` replication like [`resources`](Self::resources) —\r\n    /// no new channel, no new merge key. Additive serde-default: an N-1 row\r\n    /// (no field) parses clean and resolves to \"not provably user-backed\"\r\n    /// (the fail-closed re-stamp; rollout grace).\r\n    // [impl->REQ-MSG-6]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub endpoint_type: Option<String>,\r\n    /// Whether this instance is **bound** (a bound live broker session) vs\r\n    /// **unbound** (a live broker session that has not bound yet — pre-bind,\r\n    /// rc-attachable but not message-addressable). The unified subnet-display\r\n    /// parity datum (REQ-SUBNET-DISPLAY-PARITY): a remote viewer renders the\r\n    /// bound/unbound fill exactly as the home node does, instead of reducing\r\n    /// every remote row to plain green. `true` ⇒ bound; `false` ⇒ unbound (or a\r\n    /// cold/suspended perch — `bound=false` there is harmless, the picker keys\r\n    /// the unbound *display* on `Status::Active|Dormant && !bound`). Node-level\r\n    /// data riding the instance row like [`endpoint_type`](Self::endpoint_type)\r\n    /// — same epoch lease, same visibility gate, no new channel. Additive\r\n    /// serde-default: an N-1 row (no field) parses clean and defaults to `true`\r\n    /// (bound), the conservative no-special-display posture (a row that omits\r\n    /// the field renders as a plain bound endpoint, never a phantom unbound).\r\n    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n    #[serde(default = \"default_bound\")]\r\n    pub bound: bool,\r\n    /// The node currently **controlling** this instance's session (its\r\n    /// broker-owned `driven_by`), if any — the driver node hex. Threaded so a\r\n    /// remote viewer renders the blue \"controlled\" state + the desc-pane\r\n    /// `controlled by <node>` pin for an endpoint another node is driving, not\r\n    /// just the local node's own perches (REQ-SUBNET-DISPLAY-PARITY). Node-level\r\n    /// data riding the instance row; additive serde-default: an N-1 row parses\r\n    /// clean to `None` (no controller — the existing remote behaviour).\r\n    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub controller_node: Option<String>,\r\n    /// Whether this instance is **harness-only** — a LiveAgent with no broker\r\n    /// PTY seat (harness-hosted, not rc-controllable). Threaded so a remote\r\n    /// viewer renders the amber-hollow \"harness-only\" state instead of plain\r\n    /// green (REQ-SUBNET-DISPLAY-PARITY); mirrors the home node's\r\n    /// `controllable == Some(false)` derivation for a `live_agent`. Node-level\r\n    /// data riding the instance row; additive serde-default: an N-1 row parses\r\n    /// clean to `false` (not harness-only — the conservative posture).\r\n    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n    #[serde(default, skip_serializing_if = \"is_false\")]\r\n    pub harness_only: bool,\r\n    /// The endpoint's advertised harness **adapter** (composite `<adapter>[:profile]`,\r\n    /// [`spt_store::info::Info::adapter`]) — gossiped so a remote picker's desc pane\r\n    /// shows the real harness instead of the resources *blurb* masquerading as one\r\n    /// (#4, REQ-GOSSIP-ADAPTER-PROJECTS: `from_resource_row` used to pass the blurb as\r\n    /// the adapter). Node-level data riding the instance row like\r\n    /// [`endpoint_type`](Self::endpoint_type) — same epoch lease, same visibility gate.\r\n    /// Additive serde-default: an N-1 row parses clean to `None` (the picker renders `-`).\r\n    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub adapter: Option<String>,\r\n    /// The endpoint's **recent projects**, newest-first, project IDs ONLY\r\n    /// (owlery-internal excluded, bounded) — gossiped so a remote picker/list renders\r\n    /// real project history instead of the hardcoded empty `from_resource_row` used to\r\n    /// fake (#4). Project IDs only: dirs leak usernames/dir structure across nodes, so\r\n    /// they never cross the wire (the local Choose-project step keeps its own dirs).\r\n    /// Rides the instance row; additive serde-default: an N-1 row parses clean to empty.\r\n    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n    #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\r\n    pub recent_projects: Vec<String>,\r\n    /// Whether ANY operator (local OR remote) currently drives this instance — the\r\n    /// broker-owned any-controller marker ([`spt_store::info::Info::controlled`]).\r\n    /// Gossiped **explicitly**, NOT derived from [`controller_node`](Self::controller_node):\r\n    /// a LOCALLY-controlled endpoint has a remote-only `driven_by=None` (KH 7.15), so a\r\n    /// remote viewer reading controlled-ness off the WHO datum would undercount the\r\n    /// local-controller case (`controlled=true` yet no gossiped driver) and render it\r\n    /// plain ONLINE — reopening the #3 hole cross-node (doyle ruling). This carries the\r\n    /// raw truth; `controller_node` stays the separate WHO datum (may be `None` while\r\n    /// `controlled=true`). Additive serde-default: an N-1 row parses clean to `false`.\r\n    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n    #[serde(default, skip_serializing_if = \"is_false\")]\r\n    pub controlled: bool,\r\n}\r\n\r\n/// The serde default for [`Instance::bound`]: an N-1 row (authored before the\r\n/// field existed) is treated as **bound** — the conservative posture (a\r\n/// field-less row renders as a plain bound endpoint, never a phantom unbound).\r\n// [impl->REQ-SUBNET-DISPLAY-PARITY]\r\nfn default_bound() -> bool {\r\n    true\r\n}\r\n\r\n/// `skip_serializing_if` for the `false` (default) case of\r\n/// [`Instance::harness_only`] — keeps the wire compact (the common case omits\r\n/// the field, exactly as an N-1 row does).\r\n#[allow(clippy::trivially_copy_pass_by_ref)]\r\nfn is_false(b: &bool) -> bool {\r\n    !*b\r\n}\r\n\r\n/// One node-level label datum (REQ-SUBNET-3 endpoint-less gap): a node's\r\n/// advertised hostname, carried INDEPENDENT of endpoint rows so a node with\r\n/// **zero endpoints** still names itself to peers (the per-row\r\n/// [`Instance::node_label`] only rides endpoint rows, so an endpoint-less node\r\n/// otherwise renders a bare key-prefix). Merged under the same\r\n/// strictly-greater-epoch lease as instance rows; evicted with the node.\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct NodeLabel {\r\n    /// The node's advertised label (its OS hostname).\r\n    pub label: String,\r\n    /// The lease epoch (strictly-greater supersedes — the instance-row rule).\r\n    pub epoch: u64,\r\n}\r\n\r\n/// The per-subnet registry: `endpoint_id → [instance...]`. `BTreeMap`/`Vec` for stable,\r\n/// deterministic serde ordering (no hash-seed nondeterminism across nodes).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct SubnetRegistry {\r\n    #[serde(default)]\r\n    by_id: BTreeMap<String, Vec<Instance>>,\r\n    /// Node-level labels (`node_hex → label`), the endpoint-less carrier\r","truncated":true,"truncatedBy":"lines","totalLines":2572,"totalBytes":16238,"outputLines":263,"outputBytes":16238,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Per-subnet eventually-consistent endpoint registry (D3a/b/d/e, ADR-0006).\r\n//!\r\n//! Maps `endpoint_id → [instance...]` within one subnet, where an [`Instance`] is the\r\n//! same logical endpoint as seen running on a particular node, with a liveness\r\n//! [`Status`] and the author node's monotonic [`epoch`](Instance::epoch). The same\r\n//! `endpoint_id` can hold several instances (the node-global mind advertised from more\r\n//! than one machine); picking the right one is the resolution policy's job (D3c) — this\r\n//! module owns the **data model + the merge lease**, not resolution.\r\n//!\r\n//! ## The epoch lease (#8)\r\n//!\r\n//! Registry updates arrive out of order across a partition or under clock skew. A\r\n//! lagging node could re-announce a stale `Active` after the endpoint has actually gone\r\n//! `Offline`. [`merge_instance`] defends against this with a **version-vector** rule\r\n//! keyed on `(endpoint_id, node)`: an incoming update wins iff its epoch is **strictly\r\n//! greater** than the stored one for that node. An equal or lower epoch is dropped as\r\n//! stale — so a newer `Offline` (higher epoch) can never be clobbered by a lagging\r\n//! `Active` (lower epoch). The epoch is the per-node monotonic counter\r\n//! ([`spt_store::epoch`]), NEVER wall-clock. Cross-node *replication* of these updates\r\n//! rides the transport (D4); the merge seam is identical for local and wire-delivered\r\n//! updates.\r\n//!\r\n//! ## Join-time bare-id collision check (D3d)\r\n//!\r\n//! Bare-id uniqueness holds *within* a subnet (ADR-0006 §1): advertising a joining\r\n//! endpoint whose id another node already holds in the target subnet refuses via\r\n//! [`join_endpoint`](SubnetRegistry::join_endpoint) / [`JoinCollision`] — the clash is\r\n//! surfaced and resolved (rename, D3f) before advertisement. Per-subnet: the same bare\r\n//! name as *distinct* endpoints in different subnets is legal; the resolver forces\r\n//! qualification (D3c).\r\n//!\r\n//! ## Visibility enforcement (D3e)\r\n//!\r\n//! Excluded = **not advertised AND not routable** (ADR-0006 §6). The policy lives in\r\n//! [`spt_store::visibility::VisibilityStore`]; this module enforces it at both\r\n//! registry boundaries: [`advertise_if_visible`] keeps hidden endpoints out of the\r\n//! registry entirely, and [`resolve_visible`](SubnetRegistry::resolve_visible) /\r\n//! [`resolve_across_visible`] refuse to route to them (even via an explicit `@node`\r\n//! pin). Exclusion removes a subnet from the cross-subnet ambiguity set — hidden in\r\n//! one subnet, visible in another resolves cleanly in the visible one.\r\n// [impl->REQ-INST-7]\r\n\r\nuse std::collections::BTreeMap;\r\n\r\nuse serde::{Deserialize, Serialize};\r\nuse spt_proto::addr::Address;\r\nuse spt_store::visibility::VisibilityStore;\r\n\r\n/// An endpoint instance's liveness within a subnet. Ordering carries no precedence —\r\n/// precedence is the epoch's job; status is just the advertised state.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]\r\npub enum Status {\r\n    /// Live and currently reachable.\r\n    Active,\r\n    /// Known but idle (e.g. a paused live agent) — addressable, not currently serving.\r\n    Dormant,\r\n    /// Resting **cold** (D9-2, REQ-INST-3): the harness session is closed,\r\n    /// resumed-on-wake. Still addressable (`spt wake ling@desktop` must route) —\r\n    /// only `Offline` is unroutable. Additive variant on a pre-release fleet;\r\n    /// the NDJSON decode posture (unknown-kind skip) is the registry record's\r\n    /// forward-compat story.\r\n    // [impl->REQ-INST-3]\r\n    Suspended,\r\n    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active\r\n    /// suppression can be reasoned about; the resolver skips it.\r\n    Offline,\r\n}\r\n\r\n/// One endpoint as seen on one node: the node it runs on, its advertised [`Status`], and\r\n/// the author node's monotonic [`epoch`](Self::epoch) (the merge-precedence key).\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct Instance {\r\n    /// The node this instance runs on (node-qualifier in `id@node` addressing).\r\n    pub node: String,\r\n    /// Advertised liveness.\r\n    pub status: Status,\r\n    /// The author node's per-node monotonic epoch at the time of this update. The sole\r\n    /// precedence key in [`merge_instance`] — strictly-greater wins.\r\n    pub epoch: u64,\r\n    /// The resource-advertisement blurb (D9-4, REQ-INST-14 — CONTEXT §resource\r\n    /// advertisement): a free-text \"yellow-pages\" line describing what this\r\n    /// endpoint can serve. **Not a separate registry** — a field on the\r\n    /// instance row, riding the existing epoch lease + `RegistryUpdate`\r\n    /// replication (a blurb update is ordered like any instance update);\r\n    /// the \"subnet resource registry\" is a [projection](resource_projection)\r\n    /// of these rows. Additive serde-default: absent = no blurb, and a\r\n    /// pre-D9-4 record parses clean.\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub resources: Option<String>,\r\n    /// The presence datum (M5-D6a, REQ-PRES-1 — CONTEXT §presence): this\r\n    /// instance's `last_active_ms` recency stamp as of the authoring node's\r\n    /// advertisement scan. **Not a separate gossip channel** — a field on the\r\n    /// instance row riding the existing epoch lease + `RegistryUpdate`\r\n    /// replication, exactly like [`resources`](Self::resources); subnet-wide\r\n    /// `(last_active_node, last_active_endpoint, ts)` is a projection of\r\n    /// these rows. Wall-clock ms, cross-compared ONLY as a routing heuristic\r\n    /// (most-recently-active resolution) — the epoch stays the sole merge-\r\n    /// precedence key; clock skew can cost a suboptimal surface target, never\r\n    /// a wrong merge. Additive serde-default: a pre-D6 row parses clean.\r\n    // [impl->REQ-PRES-1]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub last_active_ms: Option<u64>,\r\n    /// The authoring node's subnet-broadcast shell adapters (M5-D8c — CONTEXT\r\n    /// §Shell model \"Discovery scope\": shell adapters on *other* subnet nodes\r\n    /// with `broadcast = subnet` surface to agents). **Not a shell-instance\r\n    /// registry** (shell perches stay private to the owner link, CONTEXT\r\n    /// \"Not in the subnet registry\") — the names of `kind=\"shell\"` adapters\r\n    /// REGISTERED on the authoring node that chose subnet-wide broadcast,\r\n    /// riding the instance row like [`resources`](Self::resources) does\r\n    /// (same epoch lease, same visibility gate, no new channel; node-level\r\n    /// data carried per-row because the rows are the distribution).\r\n    /// Discovery only: spawning one rides the deferred instantiate-anywhere\r\n    /// gate. Additive serde-default: a pre-D8 row parses clean.\r\n    // [impl->REQ-EP-5]\r\n    #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\r\n    pub shell_adapters: Vec<String>,\r\n    /// The authoring node's human-readable label (M7 D2, REQ-SUBNET-3 —\r\n    /// CONTEXT §node label): defaults to the node's OS hostname, re-checked\r\n    /// at daemon startup (a hostname change updates the advertised label).\r\n    /// Display + addressing sugar only — the pubkey stays the identity; views\r\n    /// render `LABEL (keyprefix…)`. **Not a separate channel** — node-level\r\n    /// data riding the instance row like [`resources`](Self::resources) does\r\n    /// (the rows are the distribution; same epoch lease, same visibility\r\n    /// gate). Additive serde-default: a pre-M7 row parses clean.\r\n    // [impl->REQ-SUBNET-3]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub node_label: Option<String>,\r\n    /// The authoring node's stable machine identifier, **hashed** (M8\r\n    /// decision 13/22, REQ-SUBNET-7): domain-separated SHA-256 of the OS\r\n    /// machine id (`/etc/machine-id` / Windows `MachineGuid`; spt-minted\r\n    /// persisted UUID where unreadable) — never the raw id on the wire.\r\n    /// The re-pair overwrite anchor: a completed pairing ceremony presenting\r\n    /// the same node label AND the same machine id as an existing trusted\r\n    /// row marks that row's identity as superseded (the machine re-paired\r\n    /// under a new key). Node-level data riding the instance row like\r\n    /// [`node_label`](Self::node_label) (the rows are the distribution;\r\n    /// same epoch lease, same visibility gate). Additive serde-default: a\r\n    /// pre-M8 row parses clean.\r\n    // [impl->REQ-SUBNET-7]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub machine_id: Option<String>,\r\n    /// The endpoint's advertised **type tag** (M9, REQ-MSG-6 — CONTEXT\r\n    /// §Gateway): the open-type tag this endpoint binds under\r\n    /// ([`spt_proto::endpoint::EndpointType::as_tag`]; `\"gateway\"` for a\r\n    /// Gateway surface). The cross-node `user-msg` identity gate reads this:\r\n    /// `receive_wan` resolves the origin endpoint's advertised type, keyed by\r\n    /// the QUIC-proven origin node, and honors a `user-msg` only from a\r\n    /// Gateway-typed origin (REQ-MSG-5 over the WAN). A node advertises only\r\n    /// its **own** endpoints' types (no cross-node authorship); the field\r\n    /// extends REQ-INST-7's data model and rides the existing epoch lease +\r\n    /// `RegistryUpdate` replication like [`resources`](Self::resources) —\r\n    /// no new channel, no new merge key. Additive serde-default: an N-1 row\r\n    /// (no field) parses clean and resolves to \"not provably user-backed\"\r\n    /// (the fail-closed re-stamp; rollout grace).\r\n    // [impl->REQ-MSG-6]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub endpoint_type: Option<String>,\r\n    /// Whether this instance is **bound** (a bound live broker session) vs\r\n    /// **unbound** (a live broker session that has not bound yet — pre-bind,\r\n    /// rc-attachable but not message-addressable). The unified subnet-display\r\n    /// parity datum (REQ-SUBNET-DISPLAY-PARITY): a remote viewer renders the\r\n    /// bound/unbound fill exactly as the home node does, instead of reducing\r\n    /// every remote row to plain green. `true` ⇒ bound; `false` ⇒ unbound (or a\r\n    /// cold/suspended perch — `bound=false` there is harmless, the picker keys\r\n    /// the unbound *display* on `Status::Active|Dormant && !bound`). Node-level\r\n    /// data riding the instance row like [`endpoint_type`](Self::endpoint_type)\r\n    /// — same epoch lease, same visibility gate, no new channel. Additive\r\n    /// serde-default: an N-1 row (no field) parses clean and defaults to `true`\r\n    /// (bound), the conservative no-special-display posture (a row that omits\r\n    /// the field renders as a plain bound endpoint, never a phantom unbound).\r\n    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n    #[serde(default = \"default_bound\")]\r\n    pub bound: bool,\r\n    /// The node currently **controlling** this instance's session (its\r\n    /// broker-owned `driven_by`), if any — the driver node hex. Threaded so a\r\n    /// remote viewer renders the blue \"controlled\" state + the desc-pane\r\n    /// `controlled by <node>` pin for an endpoint another node is driving, not\r\n    /// just the local node's own perches (REQ-SUBNET-DISPLAY-PARITY). Node-level\r\n    /// data riding the instance row; additive serde-default: an N-1 row parses\r\n    /// clean to `None` (no controller — the existing remote behaviour).\r\n    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub controller_node: Option<String>,\r\n    /// Whether this instance is **harness-only** — a LiveAgent with no broker\r\n    /// PTY seat (harness-hosted, not rc-controllable). Threaded so a remote\r\n    /// viewer renders the amber-hollow \"harness-only\" state instead of plain\r\n    /// green (REQ-SUBNET-DISPLAY-PARITY); mirrors the home node's\r\n    /// `controllable == Some(false)` derivation for a `live_agent`. Node-level\r\n    /// data riding the instance row; additive serde-default: an N-1 row parses\r\n    /// clean to `false` (not harness-only — the conservative posture).\r\n    // [impl->REQ-SUBNET-DISPLAY-PARITY]\r\n    #[serde(default, skip_serializing_if = \"is_false\")]\r\n    pub harness_only: bool,\r\n    /// The endpoint's advertised harness **adapter** (composite `<adapter>[:profile]`,\r\n    /// [`spt_store::info::Info::adapter`]) — gossiped so a remote picker's desc pane\r\n    /// shows the real harness instead of the resources *blurb* masquerading as one\r\n    /// (#4, REQ-GOSSIP-ADAPTER-PROJECTS: `from_resource_row` used to pass the blurb as\r\n    /// the adapter). Node-level data riding the instance row like\r\n    /// [`endpoint_type`](Self::endpoint_type) — same epoch lease, same visibility gate.\r\n    /// Additive serde-default: an N-1 row parses clean to `None` (the picker renders `-`).\r\n    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n    #[serde(default, skip_serializing_if = \"Option::is_none\")]\r\n    pub adapter: Option<String>,\r\n    /// The endpoint's **recent projects**, newest-first, project IDs ONLY\r\n    /// (owlery-internal excluded, bounded) — gossiped so a remote picker/list renders\r\n    /// real project history instead of the hardcoded empty `from_resource_row` used to\r\n    /// fake (#4). Project IDs only: dirs leak usernames/dir structure across nodes, so\r\n    /// they never cross the wire (the local Choose-project step keeps its own dirs).\r\n    /// Rides the instance row; additive serde-default: an N-1 row parses clean to empty.\r\n    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n    #[serde(default, skip_serializing_if = \"Vec::is_empty\")]\r\n    pub recent_projects: Vec<String>,\r\n    /// Whether ANY operator (local OR remote) currently drives this instance — the\r\n    /// broker-owned any-controller marker ([`spt_store::info::Info::controlled`]).\r\n    /// Gossiped **explicitly**, NOT derived from [`controller_node`](Self::controller_node):\r\n    /// a LOCALLY-controlled endpoint has a remote-only `driven_by=None` (KH 7.15), so a\r\n    /// remote viewer reading controlled-ness off the WHO datum would undercount the\r\n    /// local-controller case (`controlled=true` yet no gossiped driver) and render it\r\n    /// plain ONLINE — reopening the #3 hole cross-node (doyle ruling). This carries the\r\n    /// raw truth; `controller_node` stays the separate WHO datum (may be `None` while\r\n    /// `controlled=true`). Additive serde-default: an N-1 row parses clean to `false`.\r\n    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\r\n    #[serde(default, skip_serializing_if = \"is_false\")]\r\n    pub controlled: bool,\r\n}\r\n\r\n/// The serde default for [`Instance::bound`]: an N-1 row (authored before the\r\n/// field existed) is treated as **bound** — the conservative posture (a\r\n/// field-less row renders as a plain bound endpoint, never a phantom unbound).\r\n// [impl->REQ-SUBNET-DISPLAY-PARITY]\r\nfn default_bound() -> bool {\r\n    true\r\n}\r\n\r\n/// `skip_serializing_if` for the `false` (default) case of\r\n/// [`Instance::harness_only`] — keeps the wire compact (the common case omits\r\n/// the field, exactly as an N-1 row does).\r\n#[allow(clippy::trivially_copy_pass_by_ref)]\r\nfn is_false(b: &bool) -> bool {\r\n    !*b\r\n}\r\n\r\n/// One node-level label datum (REQ-SUBNET-3 endpoint-less gap): a node's\r\n/// advertised hostname, carried INDEPENDENT of endpoint rows so a node with\r\n/// **zero endpoints** still names itself to peers (the per-row\r\n/// [`Instance::node_label`] only rides endpoint rows, so an endpoint-less node\r\n/// otherwise renders a bare key-prefix). Merged under the same\r\n/// strictly-greater-epoch lease as instance rows; evicted with the node.\r\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct NodeLabel {\r\n    /// The node's advertised label (its OS hostname).\r\n    pub label: String,\r\n    /// The lease epoch (strictly-greater supersedes — the instance-row rule).\r\n    pub epoch: u64,\r\n}\r\n\r\n/// The per-subnet registry: `endpoint_id → [instance...]`. `BTreeMap`/`Vec` for stable,\r\n/// deterministic serde ordering (no hash-seed nondeterminism across nodes).\r\n#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]\r\npub struct SubnetRegistry {\r\n    #[serde(default)]\r\n    by_id: BTreeMap<String, Vec<Instance>>,\r\n    /// Node-level labels (`node_hex → label`), the endpoint-less carrier\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,281]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-net\\src\\net\\registry.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":2572,"totalBytes":16238,"outputLines":263,"outputBytes":16238,"shownRange":{"start":1,"end":263},"nextOffset":264}}},"isError":false,"timestamp":1784197455201}}
{"type":"message","id":"3db540c9","parentId":"ccca4cae","timestamp":"2026-07-16T10:24:15.220Z","message":{"role":"toolResult","toolCallId":"call_YymgFwnXn2VWJKPL9IXkif2L|fc_017bff223290b7ba016a58b14f1d54819aa895a5d090c5a833","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\n[Showing lines 1-223 of 3214. Use :224 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","truncated":true,"truncatedBy":"lines","totalLines":3214,"totalBytes":10555,"outputLines":223,"outputBytes":10555,"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","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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":3214,"totalBytes":10555,"outputLines":223,"outputBytes":10555,"shownRange":{"start":1,"end":223},"nextOffset":224}}},"isError":false,"timestamp":1784197455220}}
{"type":"message","id":"a8ff84e1","parentId":"3db540c9","timestamp":"2026-07-16T10:24:15.339Z","message":{"role":"toolResult","toolCallId":"call_d4jZ4nQh6KkkrIOSreAsQ4Vc|fc_017bff223290b7ba016a58b14f1d38819aa944f37440654737","toolName":"read","content":[{"type":"text","text":"fn place_binary(src: &std::path::Path, target: &std::path::Path) -> Result<bool, String> {\n…\n    std::fs::create_dir_all(dir).map_err(|e| format!(\"create {}: {e}\", dir.display()))?;\n    // Stage beside the target so the final rename is same-volume atomic; the\n    // staged name carries the pid so concurrent installs never collide.\n    let staged = dir.join(format!(\".spt-install-{}\", std::process::id()));\n    std::fs::copy(src, &staged).map_err(|e| format!(\"stage {}: {e}\", staged.display()))?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;\n        let _ = std::fs::set_permissions(&staged, std::fs::Permissions::from_mode(0o755));\n    }\n    std::fs::rename(&staged, target).map_err(|e| {\n        let _ = std::fs::remove_file(&staged);\n        format!(\n            \"place {}: {e} (is spt running from there? stop it and re-run)\",\n            target.display()\n        )\n    })?;\n    Ok(true)\n}\n\n/// `spt install` — the self-install verb (ADR-0036 §3, bootstrap path b).\n/// Non-interactive, idempotent, refuses cross-platform binaries. Prints the\n/// absolute target path: the PATH change never reaches the invoking terminal.\n// [impl->REQ-INSTALL-BOOTSTRAP-VERB]\nfn cmd_install(dir: Option<std::path::PathBuf>, no_path: bool) -> i32 {\n    let stamp = spt_daemon::current_platform();\n    if let Err(e) = install_platform_check(stamp, host_arch_probe().as_deref()) {\n        eprintln!(\"INSTALL_REFUSED:PlatformMismatch — {e}\");\n        return 1;\n    }\n    let src = match std::env::current_exe() {\n        Ok(p) => p,\n        Err(e) => {\n            eprintln!(\"INSTALL_FAIL: cannot resolve the running exe: {e}\");\n            return 1;\n        }\n    };\n    let dir = dir.unwrap_or_else(canonical_install_dir);\n    let target = dir.join(if cfg!(windows) { \"spt.exe\" } else { \"spt\" });\n    match place_binary(&src, &target) {\n        Ok(true) => println!(\"INSTALL_OK: placed {}\", target.display()),\n        Ok(false) => println!(\"INSTALL_OK: already installed at {}\", target.display()),\n        Err(e) => {\n            eprintln!(\"INSTALL_FAIL: {e}\");\n            return 1;\n        }\n    }\n    if !no_path {\n        match register_user_path(&dir) {\n            Ok(Some(note)) => println!(\"spt install: {note}\"),\n            Ok(None) => {}\n            Err(e) => {\n                // PATH failure degrades loud but never fails the placement —\n                // the printed absolute path keeps the node usable.\n                eprintln!(\"INSTALL_PATH_SKIPPED: {e}\");\n                eprintln!(\n                    \"spt install: invoke it absolutely ({}) or add {} to PATH yourself\",\n                    target.display(),\n                    dir.display()\n                );\n            }\n        }\n    }\n    println!(\n        \"spt install: in THIS terminal invoke it as {}; self-update now rides the \\\n         gh channel (`spt update fetch`)\",\n        target.display()\n    );\n    0\n}\n\n/// Build the `gh api` command (template + keys) that prints `repo`'s latest\n/// release tag to stdout. Pure — the caller runs it bounded. The `gh` CLI honors\n/// OAuth + `GH_TOKEN`, so this reads a private repo with no token in spt's hands.\n// [impl->REQ-ADAPTER-GH-TRANSPORT]\nfn gh_version_command(repo: &str) -> (String, std::collections::BTreeMap<String, String>) {\n    (\n        \"gh api repos/{repo}/releases/latest --jq .tag_name\".to_string(),\n        std::collections::BTreeMap::from([(\"repo\".to_string(), repo.to_string())]),\n    )\n}\n\n/// Build the `gh release download` command (template + keys) that writes `asset`\n/// from `repo` into `out_dir`. A concrete `tag` downloads that release; `None`\n/// downloads the latest (gh's no-tag-arg form — `latest` is NOT a literal tag).\n/// Pure — the caller runs it bounded. (REQ-ADAPTER-GH-TRANSPORT)\n// [impl->REQ-ADAPTER-GH-TRANSPORT]\nfn gh_download_command(\n    repo: &str,\n    tag: Option<&str>,\n    asset: &str,\n    out_dir: &std::path::Path,\n) -> (String, std::collections::BTreeMap<String, String>) {\n    let mut keys = std::collections::BTreeMap::from([\n        (\"repo\".to_string(), repo.to_string()),\n        (\"asset\".to_string(), asset.to_string()),\n        (\"dir\".to_string(), out_dir.to_string_lossy().into_owned()),\n    ]);\n    let template = match tag {\n        Some(t) => {\n            keys.insert(\"tag\".to_string(), t.to_string());\n            \"gh release download {tag} --repo {repo} --pattern {asset} --dir {dir} --clobber\"\n                .to_string()\n        }\n        None => {\n            \"gh release download --repo {repo} --pattern {asset} --dir {dir} --clobber\".to_string()\n        }\n    };\n    (template, keys)\n}\n\n/// Fetch a release asset's raw bytes via the resolved transport. HTTPS does a\n/// direct GET of the origin asset URL; `gh` shells `gh release download` into\n/// `scratch_dir` (the private-repo path) and reads the file back. `tag` is the\n/// concrete tag (e.g. `v0.13.2`) or `None` for the latest release.\n// [impl->REQ-ADAPTER-GH-TRANSPORT]\nfn fetch_release_asset_bytes(\n    repo: &str,\n    tag: Option<&str>,\n    asset: &str,\n    transport: EffectiveTransport,\n    scratch_dir: &std::path::Path,\n) -> Result<Vec<u8>, String> {\n    match transport {\n        EffectiveTransport::Https => {\n            let url = origin_asset_url(repo, tag, asset);\n            http_get_bytes(&release_http_client()?, &url)\n        }\n        EffectiveTransport::Gh => {\n            std::fs::create_dir_all(scratch_dir).map_err(|e| e.to_string())?;\n            let (template, keys) = gh_download_command(repo, tag, asset, scratch_dir);\n            match spt_runtime::run_bounded_command(\n                &template,\n                &keys,\n                Duration::from_secs(300),\n                None,\n            ) {\n                Ok(out) if out.success() => {\n                    let path = scratch_dir.join(asset);\n                    let bytes = std::fs::read(&path).map_err(|e| e.to_string());\n                    let _ = std::fs::remove_file(&path);\n                    bytes\n                }\n                Ok(out) => Err(format!(\"gh release download exit {:?}\", out.status_code)),\n                Err(e) => Err(e.to_string()),\n            }\n        }\n    }\n}\n\n/// Why a `.spt` archive could not be extracted into the adapter install dir.\n/// `NoArtifactForPlatform` is the typed multi-platform rejection (ADR-0024,\n/// mirrors ADR-0016's `RejectReason`) — a fat archive that carries other\n/// platforms' triple subdirs but not this node's; never a silent no-op.\n#[derive(Debug)]\nenum ExtractError {\n    /// A multi-platform archive (one or more known triple subdirs) does not\n    /// carry the recipient's own triple.\n    NoArtifactForPlatform { platform: String },\n    /// The bounded `tar` extract itself failed (non-zero exit / spawn error).\n    Tar(String),\n    /// A filesystem step (staging, moving the selected tree) failed.\n    Io(String),\n}\n\nimpl std::fmt::Display for ExtractError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            ExtractError::NoArtifactForPlatform { platform } => {\n                write!(f, \"NoArtifactForPlatform({platform})\")\n            }\n            ExtractError::Tar(e) => write!(f, \"tar: {e}\"),\n            ExtractError::Io(e) => write!(f, \"io: {e}\"),\n        }\n    }\n}\n\n/// How a `.spt` archive's root is laid out (ADR-0024).\n#[derive(Debug, PartialEq, Eq)]\nenum ArchiveLayout {\n…\n}\n\n[Showing lines 5629-5808 of 18900. Use :5809 to continue]"}],"details":{"truncation":{"content":"    std::fs::create_dir_all(dir).map_err(|e| format!(\"create {}: {e}\", dir.display()))?;\r\n    // Stage beside the target so the final rename is same-volume atomic; the\r\n    // staged name carries the pid so concurrent installs never collide.\r\n    let staged = dir.join(format!(\".spt-install-{}\", std::process::id()));\r\n    std::fs::copy(src, &staged).map_err(|e| format!(\"stage {}: {e}\", staged.display()))?;\r\n    #[cfg(unix)]\r\n    {\r\n        use std::os::unix::fs::PermissionsExt;\r\n        let _ = std::fs::set_permissions(&staged, std::fs::Permissions::from_mode(0o755));\r\n    }\r\n    std::fs::rename(&staged, target).map_err(|e| {\r\n        let _ = std::fs::remove_file(&staged);\r\n        format!(\r\n            \"place {}: {e} (is spt running from there? stop it and re-run)\",\r\n            target.display()\r\n        )\r\n    })?;\r\n    Ok(true)\r\n}\r\n\r\n/// `spt install` — the self-install verb (ADR-0036 §3, bootstrap path b).\r\n/// Non-interactive, idempotent, refuses cross-platform binaries. Prints the\r\n/// absolute target path: the PATH change never reaches the invoking terminal.\r\n// [impl->REQ-INSTALL-BOOTSTRAP-VERB]\r\nfn cmd_install(dir: Option<std::path::PathBuf>, no_path: bool) -> i32 {\r\n    let stamp = spt_daemon::current_platform();\r\n    if let Err(e) = install_platform_check(stamp, host_arch_probe().as_deref()) {\r\n        eprintln!(\"INSTALL_REFUSED:PlatformMismatch — {e}\");\r\n        return 1;\r\n    }\r\n    let src = match std::env::current_exe() {\r\n        Ok(p) => p,\r\n        Err(e) => {\r\n            eprintln!(\"INSTALL_FAIL: cannot resolve the running exe: {e}\");\r\n            return 1;\r\n        }\r\n    };\r\n    let dir = dir.unwrap_or_else(canonical_install_dir);\r\n    let target = dir.join(if cfg!(windows) { \"spt.exe\" } else { \"spt\" });\r\n    match place_binary(&src, &target) {\r\n        Ok(true) => println!(\"INSTALL_OK: placed {}\", target.display()),\r\n        Ok(false) => println!(\"INSTALL_OK: already installed at {}\", target.display()),\r\n        Err(e) => {\r\n            eprintln!(\"INSTALL_FAIL: {e}\");\r\n            return 1;\r\n        }\r\n    }\r\n    if !no_path {\r\n        match register_user_path(&dir) {\r\n            Ok(Some(note)) => println!(\"spt install: {note}\"),\r\n            Ok(None) => {}\r\n            Err(e) => {\r\n                // PATH failure degrades loud but never fails the placement —\r\n                // the printed absolute path keeps the node usable.\r\n                eprintln!(\"INSTALL_PATH_SKIPPED: {e}\");\r\n                eprintln!(\r\n                    \"spt install: invoke it absolutely ({}) or add {} to PATH yourself\",\r\n                    target.display(),\r\n                    dir.display()\r\n                );\r\n            }\r\n        }\r\n    }\r\n    println!(\r\n        \"spt install: in THIS terminal invoke it as {}; self-update now rides the \\\r\n         gh channel (`spt update fetch`)\",\r\n        target.display()\r\n    );\r\n    0\r\n}\r\n\r\n/// Build the `gh api` command (template + keys) that prints `repo`'s latest\r\n/// release tag to stdout. Pure — the caller runs it bounded. The `gh` CLI honors\r\n/// OAuth + `GH_TOKEN`, so this reads a private repo with no token in spt's hands.\r\n// [impl->REQ-ADAPTER-GH-TRANSPORT]\r\nfn gh_version_command(repo: &str) -> (String, std::collections::BTreeMap<String, String>) {\r\n    (\r\n        \"gh api repos/{repo}/releases/latest --jq .tag_name\".to_string(),\r\n        std::collections::BTreeMap::from([(\"repo\".to_string(), repo.to_string())]),\r\n    )\r\n}\r\n\r\n/// Build the `gh release download` command (template + keys) that writes `asset`\r\n/// from `repo` into `out_dir`. A concrete `tag` downloads that release; `None`\r\n/// downloads the latest (gh's no-tag-arg form — `latest` is NOT a literal tag).\r\n/// Pure — the caller runs it bounded. (REQ-ADAPTER-GH-TRANSPORT)\r\n// [impl->REQ-ADAPTER-GH-TRANSPORT]\r\nfn gh_download_command(\r\n    repo: &str,\r\n    tag: Option<&str>,\r\n    asset: &str,\r\n    out_dir: &std::path::Path,\r\n) -> (String, std::collections::BTreeMap<String, String>) {\r\n    let mut keys = std::collections::BTreeMap::from([\r\n        (\"repo\".to_string(), repo.to_string()),\r\n        (\"asset\".to_string(), asset.to_string()),\r\n        (\"dir\".to_string(), out_dir.to_string_lossy().into_owned()),\r\n    ]);\r\n    let template = match tag {\r\n        Some(t) => {\r\n            keys.insert(\"tag\".to_string(), t.to_string());\r\n            \"gh release download {tag} --repo {repo} --pattern {asset} --dir {dir} --clobber\"\r\n                .to_string()\r\n        }\r\n        None => {\r\n            \"gh release download --repo {repo} --pattern {asset} --dir {dir} --clobber\".to_string()\r\n        }\r\n    };\r\n    (template, keys)\r\n}\r\n\r\n/// Fetch a release asset's raw bytes via the resolved transport. HTTPS does a\r\n/// direct GET of the origin asset URL; `gh` shells `gh release download` into\r\n/// `scratch_dir` (the private-repo path) and reads the file back. `tag` is the\r\n/// concrete tag (e.g. `v0.13.2`) or `None` for the latest release.\r\n// [impl->REQ-ADAPTER-GH-TRANSPORT]\r\nfn fetch_release_asset_bytes(\r\n    repo: &str,\r\n    tag: Option<&str>,\r\n    asset: &str,\r\n    transport: EffectiveTransport,\r\n    scratch_dir: &std::path::Path,\r\n) -> Result<Vec<u8>, String> {\r\n    match transport {\r\n        EffectiveTransport::Https => {\r\n            let url = origin_asset_url(repo, tag, asset);\r\n            http_get_bytes(&release_http_client()?, &url)\r\n        }\r\n        EffectiveTransport::Gh => {\r\n            std::fs::create_dir_all(scratch_dir).map_err(|e| e.to_string())?;\r\n            let (template, keys) = gh_download_command(repo, tag, asset, scratch_dir);\r\n            match spt_runtime::run_bounded_command(\r\n                &template,\r\n                &keys,\r\n                Duration::from_secs(300),\r\n                None,\r\n            ) {\r\n                Ok(out) if out.success() => {\r\n                    let path = scratch_dir.join(asset);\r\n                    let bytes = std::fs::read(&path).map_err(|e| e.to_string());\r\n                    let _ = std::fs::remove_file(&path);\r\n                    bytes\r\n                }\r\n                Ok(out) => Err(format!(\"gh release download exit {:?}\", out.status_code)),\r\n                Err(e) => Err(e.to_string()),\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Why a `.spt` archive could not be extracted into the adapter install dir.\r\n/// `NoArtifactForPlatform` is the typed multi-platform rejection (ADR-0024,\r\n/// mirrors ADR-0016's `RejectReason`) — a fat archive that carries other\r\n/// platforms' triple subdirs but not this node's; never a silent no-op.\r\n#[derive(Debug)]\r\nenum ExtractError {\r\n    /// A multi-platform archive (one or more known triple subdirs) does not\r\n    /// carry the recipient's own triple.\r\n    NoArtifactForPlatform { platform: String },\r\n    /// The bounded `tar` extract itself failed (non-zero exit / spawn error).\r\n    Tar(String),\r\n    /// A filesystem step (staging, moving the selected tree) failed.\r\n    Io(String),\r\n}\r\n\r\nimpl std::fmt::Display for ExtractError {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        match self {\r\n            ExtractError::NoArtifactForPlatform { platform } => {\r\n                write!(f, \"NoArtifactForPlatform({platform})\")\r\n            }\r\n            ExtractError::Tar(e) => write!(f, \"tar: {e}\"),\r\n            ExtractError::Io(e) => write!(f, \"io: {e}\"),\r\n        }\r\n    }\r\n}\r\n\r\n/// How a `.spt` archive's root is laid out (ADR-0024).\r\n#[derive(Debug, PartialEq, Eq)]\r\nenum ArchiveLayout {\r","truncated":true,"truncatedBy":"lines","totalLines":13272,"totalBytes":7465,"outputLines":180,"outputBytes":7465,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn place_binary(src: &std::path::Path, target: &std::path::Path) -> Result<bool, String> {\n…\n    std::fs::create_dir_all(dir).map_err(|e| format!(\"create {}: {e}\", dir.display()))?;\r\n    // Stage beside the target so the final rename is same-volume atomic; the\r\n    // staged name carries the pid so concurrent installs never collide.\r\n    let staged = dir.join(format!(\".spt-install-{}\", std::process::id()));\r\n    std::fs::copy(src, &staged).map_err(|e| format!(\"stage {}: {e}\", staged.display()))?;\r\n    #[cfg(unix)]\r\n    {\r\n        use std::os::unix::fs::PermissionsExt;\r\n        let _ = std::fs::set_permissions(&staged, std::fs::Permissions::from_mode(0o755));\r\n    }\r\n    std::fs::rename(&staged, target).map_err(|e| {\r\n        let _ = std::fs::remove_file(&staged);\r\n        format!(\r\n            \"place {}: {e} (is spt running from there? stop it and re-run)\",\r\n            target.display()\r\n        )\r\n    })?;\r\n    Ok(true)\r\n}\r\n\r\n/// `spt install` — the self-install verb (ADR-0036 §3, bootstrap path b).\r\n/// Non-interactive, idempotent, refuses cross-platform binaries. Prints the\r\n/// absolute target path: the PATH change never reaches the invoking terminal.\r\n// [impl->REQ-INSTALL-BOOTSTRAP-VERB]\r\nfn cmd_install(dir: Option<std::path::PathBuf>, no_path: bool) -> i32 {\r\n    let stamp = spt_daemon::current_platform();\r\n    if let Err(e) = install_platform_check(stamp, host_arch_probe().as_deref()) {\r\n        eprintln!(\"INSTALL_REFUSED:PlatformMismatch — {e}\");\r\n        return 1;\r\n    }\r\n    let src = match std::env::current_exe() {\r\n        Ok(p) => p,\r\n        Err(e) => {\r\n            eprintln!(\"INSTALL_FAIL: cannot resolve the running exe: {e}\");\r\n            return 1;\r\n        }\r\n    };\r\n    let dir = dir.unwrap_or_else(canonical_install_dir);\r\n    let target = dir.join(if cfg!(windows) { \"spt.exe\" } else { \"spt\" });\r\n    match place_binary(&src, &target) {\r\n        Ok(true) => println!(\"INSTALL_OK: placed {}\", target.display()),\r\n        Ok(false) => println!(\"INSTALL_OK: already installed at {}\", target.display()),\r\n        Err(e) => {\r\n            eprintln!(\"INSTALL_FAIL: {e}\");\r\n            return 1;\r\n        }\r\n    }\r\n    if !no_path {\r\n        match register_user_path(&dir) {\r\n            Ok(Some(note)) => println!(\"spt install: {note}\"),\r\n            Ok(None) => {}\r\n            Err(e) => {\r\n                // PATH failure degrades loud but never fails the placement —\r\n                // the printed absolute path keeps the node usable.\r\n                eprintln!(\"INSTALL_PATH_SKIPPED: {e}\");\r\n                eprintln!(\r\n                    \"spt install: invoke it absolutely ({}) or add {} to PATH yourself\",\r\n                    target.display(),\r\n                    dir.display()\r\n                );\r\n            }\r\n        }\r\n    }\r\n    println!(\r\n        \"spt install: in THIS terminal invoke it as {}; self-update now rides the \\\r\n         gh channel (`spt update fetch`)\",\r\n        target.display()\r\n    );\r\n    0\r\n}\r\n\r\n/// Build the `gh api` command (template + keys) that prints `repo`'s latest\r\n/// release tag to stdout. Pure — the caller runs it bounded. The `gh` CLI honors\r\n/// OAuth + `GH_TOKEN`, so this reads a private repo with no token in spt's hands.\r\n// [impl->REQ-ADAPTER-GH-TRANSPORT]\r\nfn gh_version_command(repo: &str) -> (String, std::collections::BTreeMap<String, String>) {\r\n    (\r\n        \"gh api repos/{repo}/releases/latest --jq .tag_name\".to_string(),\r\n        std::collections::BTreeMap::from([(\"repo\".to_string(), repo.to_string())]),\r\n    )\r\n}\r\n\r\n/// Build the `gh release download` command (template + keys) that writes `asset`\r\n/// from `repo` into `out_dir`. A concrete `tag` downloads that release; `None`\r\n/// downloads the latest (gh's no-tag-arg form — `latest` is NOT a literal tag).\r\n/// Pure — the caller runs it bounded. (REQ-ADAPTER-GH-TRANSPORT)\r\n// [impl->REQ-ADAPTER-GH-TRANSPORT]\r\nfn gh_download_command(\r\n    repo: &str,\r\n    tag: Option<&str>,\r\n    asset: &str,\r\n    out_dir: &std::path::Path,\r\n) -> (String, std::collections::BTreeMap<String, String>) {\r\n    let mut keys = std::collections::BTreeMap::from([\r\n        (\"repo\".to_string(), repo.to_string()),\r\n        (\"asset\".to_string(), asset.to_string()),\r\n        (\"dir\".to_string(), out_dir.to_string_lossy().into_owned()),\r\n    ]);\r\n    let template = match tag {\r\n        Some(t) => {\r\n            keys.insert(\"tag\".to_string(), t.to_string());\r\n            \"gh release download {tag} --repo {repo} --pattern {asset} --dir {dir} --clobber\"\r\n                .to_string()\r\n        }\r\n        None => {\r\n            \"gh release download --repo {repo} --pattern {asset} --dir {dir} --clobber\".to_string()\r\n        }\r\n    };\r\n    (template, keys)\r\n}\r\n\r\n/// Fetch a release asset's raw bytes via the resolved transport. HTTPS does a\r\n/// direct GET of the origin asset URL; `gh` shells `gh release download` into\r\n/// `scratch_dir` (the private-repo path) and reads the file back. `tag` is the\r\n/// concrete tag (e.g. `v0.13.2`) or `None` for the latest release.\r\n// [impl->REQ-ADAPTER-GH-TRANSPORT]\r\nfn fetch_release_asset_bytes(\r\n    repo: &str,\r\n    tag: Option<&str>,\r\n    asset: &str,\r\n    transport: EffectiveTransport,\r\n    scratch_dir: &std::path::Path,\r\n) -> Result<Vec<u8>, String> {\r\n    match transport {\r\n        EffectiveTransport::Https => {\r\n            let url = origin_asset_url(repo, tag, asset);\r\n            http_get_bytes(&release_http_client()?, &url)\r\n        }\r\n        EffectiveTransport::Gh => {\r\n            std::fs::create_dir_all(scratch_dir).map_err(|e| e.to_string())?;\r\n            let (template, keys) = gh_download_command(repo, tag, asset, scratch_dir);\r\n            match spt_runtime::run_bounded_command(\r\n                &template,\r\n                &keys,\r\n                Duration::from_secs(300),\r\n                None,\r\n            ) {\r\n                Ok(out) if out.success() => {\r\n                    let path = scratch_dir.join(asset);\r\n                    let bytes = std::fs::read(&path).map_err(|e| e.to_string());\r\n                    let _ = std::fs::remove_file(&path);\r\n                    bytes\r\n                }\r\n                Ok(out) => Err(format!(\"gh release download exit {:?}\", out.status_code)),\r\n                Err(e) => Err(e.to_string()),\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n/// Why a `.spt` archive could not be extracted into the adapter install dir.\r\n/// `NoArtifactForPlatform` is the typed multi-platform rejection (ADR-0024,\r\n/// mirrors ADR-0016's `RejectReason`) — a fat archive that carries other\r\n/// platforms' triple subdirs but not this node's; never a silent no-op.\r\n#[derive(Debug)]\r\nenum ExtractError {\r\n    /// A multi-platform archive (one or more known triple subdirs) does not\r\n    /// carry the recipient's own triple.\r\n    NoArtifactForPlatform { platform: String },\r\n    /// The bounded `tar` extract itself failed (non-zero exit / spawn error).\r\n    Tar(String),\r\n    /// A filesystem step (staging, moving the selected tree) failed.\r\n    Io(String),\r\n}\r\n\r\nimpl std::fmt::Display for ExtractError {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        match self {\r\n            ExtractError::NoArtifactForPlatform { platform } => {\r\n                write!(f, \"NoArtifactForPlatform({platform})\")\r\n            }\r\n            ExtractError::Tar(e) => write!(f, \"tar: {e}\"),\r\n            ExtractError::Io(e) => write!(f, \"io: {e}\"),\r\n        }\r\n    }\r\n}\r\n\r\n/// How a `.spt` archive's root is laid out (ADR-0024).\r\n#[derive(Debug, PartialEq, Eq)]\r\nenum ArchiveLayout {\r\n…\n}","startLine":5618,"lineNumbers":[5618,null,5629,5630,5631,5632,5633,5634,5635,5636,5637,5638,5639,5640,5641,5642,5643,5644,5645,5646,5647,5648,5649,5650,5651,5652,5653,5654,5655,5656,5657,5658,5659,5660,5661,5662,5663,5664,5665,5666,5667,5668,5669,5670,5671,5672,5673,5674,5675,5676,5677,5678,5679,5680,5681,5682,5683,5684,5685,5686,5687,5688,5689,5690,5691,5692,5693,5694,5695,5696,5697,5698,5699,5700,5701,5702,5703,5704,5705,5706,5707,5708,5709,5710,5711,5712,5713,5714,5715,5716,5717,5718,5719,5720,5721,5722,5723,5724,5725,5726,5727,5728,5729,5730,5731,5732,5733,5734,5735,5736,5737,5738,5739,5740,5741,5742,5743,5744,5745,5746,5747,5748,5749,5750,5751,5752,5753,5754,5755,5756,5757,5758,5759,5760,5761,5762,5763,5764,5765,5766,5767,5768,5769,5770,5771,5772,5773,5774,5775,5776,5777,5778,5779,5780,5781,5782,5783,5784,5785,5786,5787,5788,5789,5790,5791,5792,5793,5794,5795,5796,5797,5798,5799,5800,5801,5802,5803,5804,5805,5806,5807,5808,null,5815]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":18900,"totalBytes":7465,"outputLines":180,"outputBytes":7465,"shownRange":{"start":5629,"end":5808},"nextOffset":5809}}},"isError":false,"timestamp":1784197455339}}
{"type":"message","id":"f10a0a92","parentId":"a8ff84e1","timestamp":"2026-07-16T10:24:27.165Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Searching exact function cmd_send**"},{"type":"toolCall","id":"call_UPFlnkqisxOPfvhr434TMDB0|fc_017bff223290b7ba016a58b15b17b4819abf6df369612b6e6b","name":"grep","arguments":{"i":"Locating current send handler","pattern":"^fn cmd_send|^pub fn cmd_send|fn receive_wan|pub fn receive_wan|WanMessage","path":"crates/spt/src/cli.rs; crates/spt-daemon/src; crates/spt/src/wansend.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_6eydQPhMa6D0dZEm9o327eqk|fc_017bff223290b7ba016a58b15b17e8819aafdf06564dc1ff60","name":"grep","arguments":{"i":"Locating current RC entry","pattern":"cmd_rc|run_rc|resolve_and_dial_owner|request_attach_endpoint|OwnerDial|NoSession|not attach","path":"crates/spt/src/cli.rs; crates/spt/src/rc.rs; crates/spt-daemon/src/attach.rs; crates/spt/src/wansend.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_zdP9qiBuGOXkwkEm58ypkWat|fc_017bff223290b7ba016a58b15b17f4819a82272ba706bffc15","name":"grep","arguments":{"i":"Locating local registry advertisement","pattern":"harness_only|controllable|bound:|Instance \\{|advertise_local|scan","path":"crates/spt-daemon/src/registryhost.rs; crates/spt-daemon/src/pump; crates/spt-daemon/src","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_r4gyfntQs8No4EOSpKaJ2C8W|fc_017bff223290b7ba016a58b15b1800819aa418b027b6a09326","name":"read","arguments":{"i":"Reading hazard elevation and attachment","path":"docs/KNOWN-HAZARDS.md","selector":"45-76,115-124,213-248"}},{"type":"toolCall","id":"call_hkIpJRrLAWwqleUbR0LkwNAc|fc_017bff223290b7ba016a58b15b1810819a89664669eaed0b49","name":"read","arguments":{"i":"Reading hosted topology decision","path":"docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md","selector":"1-120"}},{"type":"toolCall","id":"call_nlI16nX6FWCyILiGYqeIoEes|fc_017bff223290b7ba016a58b15b181c819ab24caf306d5415f0","name":"read","arguments":{"i":"Reading unbound attachment decision","path":"docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md","selector":"1-80"}},{"type":"toolCall","id":"call_hAQSyumGIpTV77r3N0vLOE7m|fc_017bff223290b7ba016a58b15b1828819a85e2fc33b9098c7f","name":"read","arguments":{"i":"Reading delivery axes decision","path":"docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md","selector":"1-180"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":30740,"output":492,"cacheRead":112128,"cacheWrite":0,"totalTokens":143360,"reasoningTokens":22,"cost":{"input":0.1537,"output":0.01476,"cacheRead":0.056063999999999996,"cacheWrite":0,"total":0.224524}},"stopReason":"toolUse","timestamp":1784197455380,"responseId":"resp_017bff223290b7ba016a58b14fac2c819abb0fe8f52f357939","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b1523840819a8552553a8a5c4074","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLFb6Hr1aRdThLEeNvwlVF-lwG7FnrAEfWvlGAYy_BWDTjDMb6fd4WxTN3DvIfYzplpR96gQSLeIB_UL3WokMMx3GR1ExkUfQPhsQCOcP3MvAlYzwzOU5ZkAM6_jMzT7o2ypjE9PrMt7i2025S_JcE1EXwktAj_Nc_LPISeMtG_AVFqp0kjxUKhp6_WiqzJqD00N6aSJCb0Dxxs7PDNG-pBHS0k_KY8xzfeeEheYKZpr9R41qDsyD9f0joqrrDTdx8w5i9CSvGjCNcAt9YK9My7K8Zd9BruKDvJg0swCfKVqM24CyD7GQoZw45E3cZ-SXPPoUg90jYxBEpuHlnVqVLQh2eeuDJkVOstAjSwPYzCz-q-0YGKHuSM773kRdbotgUeOLZBm4aqWfLc0Y-xHmeULhVQ5BMN3MWRe0IanGncgRn_UQvVwlpDqN-keGYMhmoGW7h-0ORu8auXPLvJ0hWisWJ8GyS-gco03hXuFLe1opbgJlpj9LU5j1UiuaP4Nz19J3oAAy-1j905s1OFEqBdd1_ReF-aDDVsQQg4rQp9GuPhAoCPwv7Ne4E6VZJmPlbnRgVjY7DZGTKvMFva8cx31_wW5Isdvx3GXI_0MGXihP7QRbfq69TbOKduOu6xsdgPDgZAcjR-eoDAmVXcKUtVy8hpffS4CqDfIwU-JED9Ly_qMGfrpHDBpOiHoZTJk16M_6xWI062lDSVA5YZ8TFylsm4fk4V67U633LxuTCQKD078_raS_QfdXUFSkOw3tmyMuU9ZFSncCMWddN6fGTXfoj1ZDtrDeNoAi4Ynm5_ROks_bu158KSqSvPNAhS_4dPtpVHy_VQjHNv5yNPW07H5xFt6EpKbhInmHi2d03OMJf3L7zGuJfNdFgeeW2X9mWuQj11SUSuRVEQqpi1uJbG9wjxmJaAqFAxly_kEaL_w9auH9aXiMXf2DbSfIQhvKoDB06AL80nrJkXFbkruG3jsVzVKblu5w0ora46Bk8JbmWRfEFqKjdKYteLBCKGlPxH9H2wnp3LqrwIf2EcUjYt2XQ==","summary":[{"type":"summary_text","text":"**Searching exact function cmd_send**"}]},{"id":"fc_017bff223290b7ba016a58b15b17b4819abf6df369612b6e6b","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating current send handler\",\"pattern\":\"^fn cmd_send|^pub fn cmd_send|fn receive_wan|pub fn receive_wan|WanMessage\",\"path\":\"crates/spt/src/cli.rs; crates/spt-daemon/src; crates/spt/src/wansend.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_UPFlnkqisxOPfvhr434TMDB0","name":"grep"},{"id":"fc_017bff223290b7ba016a58b15b17e8819aafdf06564dc1ff60","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating current RC entry\",\"pattern\":\"cmd_rc|run_rc|resolve_and_dial_owner|request_attach_endpoint|OwnerDial|NoSession|not attach\",\"path\":\"crates/spt/src/cli.rs; crates/spt/src/rc.rs; crates/spt-daemon/src/attach.rs; crates/spt/src/wansend.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_6eydQPhMa6D0dZEm9o327eqk","name":"grep"},{"id":"fc_017bff223290b7ba016a58b15b17f4819a82272ba706bffc15","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating local registry advertisement\",\"pattern\":\"harness_only|controllable|bound:|Instance \\\\{|advertise_local|scan\",\"path\":\"crates/spt-daemon/src/registryhost.rs; crates/spt-daemon/src/pump; crates/spt-daemon/src\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_zdP9qiBuGOXkwkEm58ypkWat","name":"grep"},{"id":"fc_017bff223290b7ba016a58b15b1800819aa418b027b6a09326","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading hazard elevation and attachment\",\"path\":\"docs/KNOWN-HAZARDS.md\",\"selector\":\"45-76,115-124,213-248\"}","call_id":"call_r4gyfntQs8No4EOSpKaJ2C8W","name":"read"},{"id":"fc_017bff223290b7ba016a58b15b1810819a89664669eaed0b49","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading hosted topology decision\",\"path\":\"docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md\",\"selector\":\"1-120\"}","call_id":"call_hkIpJRrLAWwqleUbR0LkwNAc","name":"read"},{"id":"fc_017bff223290b7ba016a58b15b181c819ab24caf306d5415f0","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading unbound attachment decision\",\"path\":\"docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md\",\"selector\":\"1-80\"}","call_id":"call_nlI16nX6FWCyILiGYqeIoEes","name":"read"},{"id":"fc_017bff223290b7ba016a58b15b1828819a85e2fc33b9098c7f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading delivery axes decision\",\"path\":\"docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md\",\"selector\":\"1-180\"}","call_id":"call_hAQSyumGIpTV77r3N0vLOE7m","name":"read"}]},"duration":11784.0764999995,"ttft":2812.1031999997795,"contextSnapshot":{"promptTokens":142868,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_UPFlnkqisxOPfvhr434TMDB0|fc_017bff223290b7ba016a58b15b17b4819abf6df369612b6e6b","toolName":"grep","startedAt":"2026-07-16T10:24:27.166Z","args":{"path":"crates/spt/src/cli.rs; crates/spt-daemon/src; crates/spt/src/wansend.rs"},"intent":"Locating current send handler"},"id":"6195316c","parentId":"f10a0a92","timestamp":"2026-07-16T10:24:27.166Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6eydQPhMa6D0dZEm9o327eqk|fc_017bff223290b7ba016a58b15b17e8819aafdf06564dc1ff60","toolName":"grep","startedAt":"2026-07-16T10:24:27.166Z","args":{"path":"crates/spt/src/cli.rs; crates/spt/src/rc.rs; crates/spt-daemon/src/attach.rs; crates/spt/src/wansend.rs"},"intent":"Locating current RC entry"},"id":"fa4749d8","parentId":"6195316c","timestamp":"2026-07-16T10:24:27.166Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_zdP9qiBuGOXkwkEm58ypkWat|fc_017bff223290b7ba016a58b15b17f4819a82272ba706bffc15","toolName":"grep","startedAt":"2026-07-16T10:24:27.166Z","args":{"path":"crates/spt-daemon/src/registryhost.rs; crates/spt-daemon/src/pump; crates/spt-daemon/src"},"intent":"Locating local registry advertisement"},"id":"d1a9014f","parentId":"fa4749d8","timestamp":"2026-07-16T10:24:27.166Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_r4gyfntQs8No4EOSpKaJ2C8W|fc_017bff223290b7ba016a58b15b1800819aa418b027b6a09326","toolName":"read","startedAt":"2026-07-16T10:24:27.166Z","args":{"path":"docs/KNOWN-HAZARDS.md"},"intent":"Reading hazard elevation and attachment"},"id":"bed7d792","parentId":"d1a9014f","timestamp":"2026-07-16T10:24:27.166Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hkIpJRrLAWwqleUbR0LkwNAc|fc_017bff223290b7ba016a58b15b1810819a89664669eaed0b49","toolName":"read","startedAt":"2026-07-16T10:24:27.166Z","args":{"path":"docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md"},"intent":"Reading hosted topology decision"},"id":"afe5fd18","parentId":"bed7d792","timestamp":"2026-07-16T10:24:27.166Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_nlI16nX6FWCyILiGYqeIoEes|fc_017bff223290b7ba016a58b15b181c819ab24caf306d5415f0","toolName":"read","startedAt":"2026-07-16T10:24:27.166Z","args":{"path":"docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md"},"intent":"Reading unbound attachment decision"},"id":"8f84e0fb","parentId":"afe5fd18","timestamp":"2026-07-16T10:24:27.166Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_hAQSyumGIpTV77r3N0vLOE7m|fc_017bff223290b7ba016a58b15b1828819a85e2fc33b9098c7f","toolName":"read","startedAt":"2026-07-16T10:24:27.166Z","args":{"path":"docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md"},"intent":"Reading delivery axes decision"},"id":"05335a41","parentId":"8f84e0fb","timestamp":"2026-07-16T10:24:27.166Z"}
{"type":"message","id":"e95765e0","parentId":"05335a41","timestamp":"2026-07-16T10:24:27.219Z","message":{"role":"toolResult","toolCallId":"call_r4gyfntQs8No4EOSpKaJ2C8W|fc_017bff223290b7ba016a58b15b1800819aa418b027b6a09326","toolName":"read","content":[{"type":"text","text":"### 2.1 Parent PID over ephemeral poll PID\n- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\n- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\n- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n\n### 2.2 Stdin session_id precedence over env\n- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\n- **Invariant:** stdin-provided session_id wins; env is fallback only.\n- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\n- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\n\n### 2.3 Binary-handoff argv schema must stay backward-compatible\n- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\n- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\n- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n\n### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\n- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes \"same gen continuing\" vs \"new gen born\".\n- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\n- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs all session continuity by **querying the broker** (`Brain::resume_sessions` over the broker's cursor-of-record), never a frame. `BrainState` / `Brain::handoff` / `Brain::snapshot` remain `pub` and compiled **only for the integration tests** (handoff/idempotent/da…\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n\n### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\n- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads \"alive\" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third categor…\n- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT \"Shell… Not in the subnet registry\") and extends it to daemon-hosted *agent* perches.\n- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.\n- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n\n---\n\n…\n### 4.3 Registry stale-entry cleanup precedes lookup\n- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n\n### 4.4 Deferred rows survive poll drain\n- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\n- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.\n- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)\n\n…\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\n\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\n- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\n- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\n- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\n- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\n- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\n\n### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\n- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\n- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\n- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\n\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\n- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one quoted token).\n- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this composes with).\n- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n\n<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->\n\n[Some lines truncated to 768 chars]"}],"details":{"displayContent":{"text":"### 2.1 Parent PID over ephemeral poll PID\n- **Failure:** orphan check polls an ephemeral listener PID; it dies and is recycled (esp. Windows); a foreign process with the recycled PID reads as alive → false-positive teardown (or false-negative).\n- **Invariant:** prefer the stable harness-session PID (`parent_pid`) over any ephemeral process PID for liveness; minimal `info.json` for supervisor-owned perches to avoid stale leaks.\n- **spt-core mapping:** session binding (parent-process-tree anchor) still applies for harness-hosted topology. For spt-hosted sessions the broker holds the child directly → liveness is the broker's held-handle state, more reliable than PID polling.\n- **Sister cite:** `src/live/wrapper/orphan.rs:141-161`; CHANGELOG v1.11.20.\n\n### 2.2 Stdin session_id precedence over env\n- **Failure:** subagent inherits a stale `OWL_SESSION_ID` env across `/clear`; hook gets two session_ids (fresh stdin, stale env) → wrong-agent binding.\n- **Invariant:** stdin-provided session_id wins; env is fallback only.\n- **spt-core mapping:** the harness-contract subcommand surface must define the same precedence for whatever identity fields hooks pass in.\n- **Sister cite:** CHANGELOG v1.35.1 \"IN-05\"; `hook_subagent_start.rs:40-51`.\n\n### 2.3 Binary-handoff argv schema must stay backward-compatible\n- **Failure:** old binary spawns new binary with old argv arity; clap rejects before state rehydration → wrapper dies unlogged.\n- **Invariant:** every newly-added handoff positional has a default; state-file rehydration happens *after* argv parse; defaults survive intermediate versions.\n- **spt-core mapping:** CRITICAL — self-update (ADR-0004) makes handoff routine. The broker↔brain IPC and any brain-relaunch argv must be versioned and forward/backward tolerant (a newer brain talks to an older broker). This is the single most update-frequency-sensitive invariant.\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:17-106`; `src/cli.rs` defaults; CHANGELOG v1.11.10.\n\n### 2.4 Generation `gen_start` always = now() on cold-start AND handoff\n- **Failure:** stale gen_start from a rehydrated state file fires time-based discriminators on the new process.\n- **Invariant:** wall-clock `gen_start` is set to `now()` on both cold-start and handoff; generation counter increments on every start/revive; session UUID captured fresh and carried so the resumed mind distinguishes \"same gen continuing\" vs \"new gen born\".\n- **spt-core mapping:** carries to the daemon's per-instance generation tracking.\n- **Restoration D3/D4 (ADR-0018):** the generation *counter* custody moved to the broker (D3-2 — it observes every brain spawn, planned or crash, and hands `{generation, start-reason}` at spawn; `gen_start` stays `now()`-fresh, never rehydrated). The brain→brain **`BrainState` *message*** (`{session_id, generation, next_seq, gen_start_ms}`) that previously carried continuity across a handoff is **retired from the production path in D4-2**: a brain the supervisor respawns cold-starts and reconstructs all session continuity by **querying the broker** (`Brain::resume_sessions` over the broker's cursor-of-record), never a frame. `BrainState` / `Brain::handoff` / `Brain::snapshot` remain `pub` and compiled **only for the integration tests** (handoff/idempotent/da…\n- **Sister cite:** `src/live/wrapper/lifecycle.rs:70`; `src/common/wrapper_state.rs`.\n\n### 2.5 Daemon-hosted endpoints have no dedicated liveness PID\n- **Failure:** the sister evaluates Psyche/perch liveness via a dedicated process PID — the wrapper's own pid in `info.json`, checked with `is_process_alive`. Under ADR-0004 the Psyche (and any spt-hosted Self) is a **loop inside the daemon**, not a separate process: it holds no dedicated pid, and its `claude`/summarizer subprocess is ephemeral (spawned per pulse/commune, then exits). If a daemon-hosted perch's `info.json` carries the **daemon's** pid, then *every* hosted endpoint shares one pid, and `is_process_alive(pid)` reads \"alive\" for a torn-down endpoint as long as the daemon runs — while `clean_stale_entries` (dead-pid deletion) can no longer distinguish a dead endpoint from a live one. The 2.1/5.1 liveness models do **not** cover this third categor…\n- **Invariant:** for **daemon-hosted** perches (Psyche; spt-hosted Self), liveness is the **daemon's authoritative in-memory endpoint table + a `status` field** on `info.json` (`online|offline|…`), **never** `is_process_alive(info.pid)`. `info.pid` for a daemon-hosted perch is at most a *hosted-by-daemon* marker (the daemon pid), not a liveness signal; registry stale-clean for these rows keys on the daemon's endpoint table, not per-row pid. This reuses the pattern already specified for **Shells** (`info.json` carries daemon-managed `status`, capability resolved by `adapter_name` — CONTEXT \"Shell… Not in the subnet registry\") and extends it to daemon-hosted *agent* perches.\n- **spt-core mapping:** the **M1/M2a interim** model keeps the Psyche/listener a real per-process owner (the `api listen` process), so its per-pid liveness (`deliver::is_online` → `info.read_pid` → `proc::is_process_alive`; `registry::clean_stale_entries`) is correct *interim*. **M3 daemon consolidation replaces it** with daemon-authoritative liveness for hosted perches. Keep the liveness check behind one resolver (mirrors `resolve_address` stale-clean) so the M3 swap is localized — do **not** let the per-pid assumption leak into new call sites.\n- **Sister cite:** `src/live/wrapper/orphan.rs` (wrapper-pid liveness); `src/common/list_filter.rs:168-175` (pid-classify); spt-core `crates/spt-store/src/{proc.rs,registry.rs}` + `crates/spt-msg/src/deliver.rs::is_online`.\n\n---\n\n…\n### 4.3 Registry stale-entry cleanup precedes lookup\n- **Failure:** sender resolves a dead process's stale TCP port → delivery to wrong/dead listener.\n- **Invariant:** clean stale entries (dead PID) before/at lookup; spool fallback is the safe path on TCP miss.\n- **spt-core mapping:** now spans the **subnet registry** (ADR-0003) — eventually-consistent across nodes. Cross-node staleness is expected; resolution policy (local → most-recent → `id@node`) must degrade to spool/relay fallback on stale hits, and never hard-fail on a stale remote entry.\n- **Sister cite:** `src/common/registry.rs:62-78`; `src/owl/send.rs`.\n\n### 4.4 Deferred rows survive poll drain\n- **Failure:** poll `drain_all` flushes a deferred (spool-only) message meant for a hook consumer → message lost.\n- **Invariant:** deferred rows are never flushed by the event-stream drain; only `drain_non_deferred_*` / `peek_all` touch them.\n- **Sister cite:** CHANGELOG v1.11.20; `src/common/spool.rs`. (See also 1.4.)\n\n…\n### 5.7 Elevated commands spawn the daemon with the wrong token  `[REQ-HAZARD-ELEVATED-DAEMON-SPAWN]`\n<!-- [doc->REQ-HAZARD-ELEVATED-DAEMON-SPAWN] -->\n- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill (hit live, M7 acceptance 2026-06-06). **Linux:** a sudo'd command spawns a root daemon and/or root-owned state — and because sudo flips `$HOME`, the daemon can mint a *different node identity* in root's universe (the very key-flip that produced the 4.10 ghost r…\n- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run` entry guard catches a *directly* elevated `spt daemon` — Linux drops privileges in-process before touching any state; Windows respawns de-elevated and exits. When no unelevated identity exists to drop to (UAC disabled, genuine root login, SYSTEM), the daemon runs …\n- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n\n<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n- **Failure:** the daemon runs DETACHED (no console, 5.6/`detached_no_inherit`). Any console-subsystem child it spawns (`git`, `taskkill`, manifest hook commands) gets a **fresh conhost with a visible window** — piped/null stdio does NOT prevent it. Field shape: the 60s sync pump's two git calls (`for-each-ref` + `rev-parse`) flashed two blank windows per minute on the user's desktop (2026-06-06).\n- **Invariant:** every short-lived console child spawned from daemon-reachable code sets `creation_flags(0x0800_0000)` (`CREATE_NO_WINDOW`). Long-lived detached children use `detached_no_inherit` (already `DETACHED_PROCESS | CREATE_NO_WINDOW`); de-elevated spawns use `CREATE_NEW_CONSOLE + SW_HIDE` (5.7 — `CreateProcessWithTokenW` rejects `CREATE_NO_WINDOW`, error 87).\n- **Test seam caveat:** window-absence is unobservable from a consoled test runner — the child inherits the runner's console and never creates a window, flag or no flag. Unit coverage asserts the flagged spawn still works (the error-87 \"flag combo breaks spawn\" regression class); window-absence was verified live by process-watch capture.\n- **spt-core mapping:** `spt-store::gitrun::run_git` (every BranchStore/ContextStore git call), `spt-daemon::shellhost::kill_shell_pid` (taskkill), `spt-runtime::run_bounded_command` (manifest hook commands), `spt-runtime::ManifestRuntime::command_for` (the one shared builder behind `spawn_session` + `run_bounded_stdin` — the notif pump's `spawn_notif_command` and the live agent's psyche/echo/turn spawns), `spt-daemon::shellwake` (already guarded). The flag lives in each shared builder, not per call site, so the invariant holds for every ManifestRuntime spawn by construction.\n- **Source:** spt-core field bug, 2026-06-06 — two blank windows flashing every 60 seconds on a desktop workstation, caught by process-spawn watcher (git.exe parent=spt daemon, conhost.exe child each).\n\n### 5.9 `Instant - Duration` underflow-panics on a freshly-booted host  `[REQ-HAZARD-INSTANT-UNDERFLOW]`\n- **Failure:** `Instant::now() - Duration::from_secs(N)` panics `overflow when subtracting duration from instant` when the process's monotonic clock is younger than `N` — i.e. the host booted less than `N` ago. The peer pump primed its cadence legs with `Instant::now() - 86_400s` to mean \"everything due now\"; on a Windows runner with sub-24h uptime the pump thread panicked at startup, so the subnet never converged (CI `pump_and_dispatch_self_drive_the_subnet` failed, run 27082417706). It is *environment-conditional* — green on any host up longer than the offset, red below it — so it slips local dev and only bites a fresh CI box or a just-rebooted machine.\n- **Invariant:** NEVER compute an instant in the past by subtracting from `Instant::now()`. Represent \"never run / due now\" as `Option<Instant> = None` and gate on forward `now.duration_since(past)` only (`peerloop::due`). No backward instant arithmetic anywhere in scheduling.\n- **Test seam caveat:** the convergence E2E only reproduces on a sub-offset-uptime host (it passed everywhere with >24h uptime). The deterministic guard is the `due(None, ..)`/`due(Some(now), ..)` unit on the extracted gate — it asserts first-tick-due with zero instant subtraction, independent of host uptime.\n- **spt-core mapping:** `spt-daemon::peerloop::due` (the sole cadence gate behind `due_reg`/`due_notif`/`due_sync`/`due_upd`); cadence legs are `Option<Instant>` seeded `None`.\n- **Source:** spt-core CI failure, 2026-06-07 — Windows runner `hfenduleam` (just booted) panicked the peer pump at the v0.1.1 release gate.\n\n### 5.10 `sudo spt` dead-ends on a user-local install (secure_path)  `[REQ-HAZARD-SUDO-SECURE-PATH]`\n- **Failure:** the elevation-gated commands (`subnet create` / `subnet join` / `show-code`) refuse when unelevated and tell the user to \"run as administrator / root\". The user types the obvious `sudo spt subnet create FOO` → `sudo: spt: command not found`. `spt` is a user-local install (`~/.local/bin`, `~/.cargo/bin`), and sudo's `secure_path` (a `/etc/sudoers` default) does NOT include those dirs, so a bare command name doesn't resolve under sudo. The guidance is a trap: it names an action that cannot work for the common install shape. Field-hit on KITSUBITO at the v0.1.1 ship.\n- **Invariant:** elevation guidance on Unix emits the binary's **absolute path** under sudo — `sudo /home/u/.local/bin/spt subnet create FOO` — reconstructed from `current_exe()` + the real argv and shell-quoted. An absolute program path is executed directly; `secure_path` only governs bare-name PATH lookup, so the absolute form always resolves. On an interactive Unix TTY the command auto-elevates (re-execs itself under sudo, the elevated child does the work and `main` de-elevates back); non-interactive or sudo-absent falls back to printing the runnable hint. Never emit a bare-name elevation instruction.\n- **Companion UX:** the post-de-elevation `DEELEVATED: running as uid N` line is internal state-safety noise — omit it from the user-facing CLI path (it confused the same field user). The detached daemon's own de-elevation log line is fine (it lands in the daemon log, not the terminal).\n- **Test seam caveat:** the sudo re-exec needs a real `sudo` + TTY (not hermetic). The deterministic guard is the pure `elevation::sudo_argv` / `print_hint_command` (assert an absolute exe path, never a bare name, + shell-quoting on the printed line) and the `decide_elevation_path` matrix (which picks inline-sudo only on an interactive Unix TTY); the exec leg is manual/kitsubito-verified.\n- **spt-core mapping:** `spt::elevation::{sudo_argv, print_hint_command, decide_elevation_path}` (pure — generalized from the M12-W4 self-elevation seam, 5.11), `spt::cli::{try_auto_elevate, with_elevation_hint}` wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`; `spt::main` de-elevation drop silenced.\n- **Source:** spt-core field report, 2026-06-07 — `reavus@KITSUBITO`, `spt` in `~/.local/bin`; the absolute-path `sudo` invocation was confirmed working before the fix landed.\n\n### 5.11 Self-elevating re-launch must re-run verbatim, never widen / inject / loop  `[REQ-HAZARD-SELF-ELEVATE]`\n- **Failure class:** a privilege-gated command (`subnet create` / `join` / `show-code`) self-elevates by re-launching itself with privilege (Windows UAC `runas`, Linux `pkexec` / a terminal-emulator `sudo`, or inline `sudo`). A careless re-launch is a security hole: widening the privilege scope (adding args), resolving the binary by a bare name (a PATH/`secure_path` hijack runs an attacker's `spt`), interpolating a crafted arg into a shell string (`sh -c \"… $id …\"` injects a second command), or re-elevating the already-elevated child (an infinite UAC/polkit loop). The user's UAC/polkit/sudo prompt is the ONLY consent gate — the mechanism must never bypass or widen it.\n- **Invariant:** self-elevation re-runs the **EXACT** original invocation with the binary's **ABSOLUTE** exe path — never adding/altering args, never a PATH-resolved bare name, never a shell-interpolated string. Every launcher passes an **argv array** (`Command::new(prog).args([...])`, never `sh -c`); the Windows `ShellExecuteW` params string (which is inherently one string) MSVC-quotes each verbatim arg so `CommandLineToArgvW` round-trips it as a single token. The elevated child drops state back to the user (composes with the 5.7 de-elevation) and **never re-elevates**: `decide_elevation_path` returns `AlreadyElevated` whenever the process is `Elevated`, on every OS (loop-safety). The unprivileged parent never pipes/captures the elevated child's stdout acro…\n- **Test seam caveat:** the real launch needs a UAC/polkit/sudo prompt (not hermetic) — manual-verify. The deterministic guards are the pure `decide_elevation_path` matrix (loop-safety: `AlreadyElevated` on every os; the os×env path order) and the argv builders (`sudo_argv` / `pkexec_argv` / `terminal_argv` assert absolute-exe + verbatim args + array; `windows_runas_params` asserts MSVC-quoting with no `cmd /c` interpolation; the crafted-arg test asserts a shell-metachar arg stays one element / one quoted token).\n- **spt-core mapping:** `spt::elevation::{decide_elevation_path, sudo_argv, pkexec_argv, terminal_argv, windows_runas_params, print_hint_command, ElevatePath}` (pure), `spt::cli::{try_auto_elevate, launch_uac_window, pause_elevated_console_if_fresh, program_on_path, first_terminal_emulator}` (impure launchers) wired into `cmd_subnet_create` / `cmd_subnet_join` / `cmd_subnet_show_code`. Companions: 5.10 (the Unix abs-path-under-sudo facet) and 5.7 (the elevated child's de-elevation drop, which this composes with).\n- **Source:** M12-W4 design (subnet QR + self-elevating window), doyle ruling `M12-W4-RULING.md` Q6 — a privilege-escalation feature carries a mandatory hazard REQ.\n\n<!-- [doc->REQ-HAZARD-WIN-PTY-PROGRAM-RESOLVE] -->","startLine":45,"lineNumbers":[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,null,115,116,117,118,119,120,121,122,123,124,125,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\KNOWN-HAZARDS.md"},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784197467219}}
{"type":"message","id":"f864f5ac","parentId":"e95765e0","timestamp":"2026-07-16T10:24:27.225Z","message":{"role":"toolResult","toolCallId":"call_UPFlnkqisxOPfvhr434TMDB0|fc_017bff223290b7ba016a58b15b17b4819abf6df369612b6e6b","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/src/\n### cli.rs\n 6454|#[allow(clippy::too_many_arguments)]\n*6455|fn cmd_send(\n 6456|    target: String,\n 6457|    from: Option<String>,\n 6458|    idle_only: bool,\n### wansend.rs\n 8|//! ([`resolve_across_visible`]: refuse-and-qualify on ambiguity, never a\n*9|//! guess), and ships one [`WanMessage`] over the broker's QUIC endpoint to\n 10|//! the resolved node. The receiving daemon's dispatcher funnels it through\n 11|//! the access gate + op-id dedup into the target's local delivery\n 12|//! (`spt-daemon::wan::receive_wan`).\n...\n 29|};\n*30|use spt_net::net::wanmsg::WanMessage;\n 31|use spt_proto::addr::Address;\n 32|use spt_store::epoch::EpochSource;\n 33|use spt_store::perch;\n...\n 207|/// The testable core: resolve against `regs`, dial via `resolver`, write one\n*208|/// `WanMessage` line, finish the stream.\n 209|#[allow(clippy::too_many_arguments)]\n 210|pub fn wan_send_with(\n 211|    brain: &mut Brain,\n...\n 255|    };\n*256|    let msg = WanMessage {\n 257|        target: address.id.clone(),\n 258|        from: from.to_string(),\n 259|        body: body.to_string(),\n...\n 854|    // [unit->REQ-NET-1] the WAN leg end to end: resolve from a registry map,\n*855|    // dial, ship one WanMessage; B's production dispatcher funnels it into\n 856|    // the target perch's spool. Ambiguity refuses (REQ-INST-10); an\n 857|    // own-node hit degrades to NotFound (the local path's report stands).\n 858|    // [unit->REQ-WAN-SEND-DELIVERY] the honest-SENT decision (#9/#10): EXACTLY the\n\n## spt-daemon/src/\n### dispatch.rs\n 97|    Notif,\n*98|    /// A WAN message feed (untagged `WanMessage` lines).\n 99|    WanMsg,\n 100|    /// A registry replication feed (untagged `RegistryUpdate` lines).\n 101|    Registry,\n...\n 627|\n*628|/// WAN message arm: decode `WanMessage` lines, funnel each through the\n 629|/// access-gate + op-id-dedup path, until the peer finishes.\n 630|// [impl->REQ-NET-1]\n 631|fn serve_wan_feed(\n### lib.rs\n 83|//! **D5a (this commit)** puts WAN message delivery on the D4 substrate: a\n*84|//! [`WanMessage`](spt_net::net::wanmsg::WanMessage) NDJSON feed over a\n 85|//! broker-owned QUIC stream funnels through [`wan::receive_wan`] — the ADR-0009\n 86|//! access-gate seam ([`access`], handshake-proven origin only, KNOWN-HAZARDS\n 87|//! 7.5) plus durable op-id dedup (7.2, wire-fed) — into the existing local\n### wan.rs\n 4|//! The sending node resolved the target (D3c policy), dialed, and wrote\n*5|//! [`WanMessage`] NDJSON lines onto a broker-owned QUIC stream (D4). This\n 6|//! module is the receiving node's funnel from a decoded record to the local\n 7|//! REQ-MSG machinery — it adds exactly two things the local path doesn't have,\n 8|//! then delegates:\n...\n 38|use spt_net::net::registry::{node_label_display, Instance};\n*39|use spt_net::net::wanmsg::{WanMessage, WanReplyDecoder};\n 40|\n 41|use crate::brain::{Brain, BrokerEvent};\n 42|use spt_proto::endpoint::EndpointType;\n...\n 196|    conn_id: u64,\n*197|    msg: &WanMessage,\n 198|) -> std::io::Result<WanRequestOutcome> {\n 199|    let opened = brain.net_open_stream(conn_id, None)?;\n 200|    // One-shot: the message is everything we send; finishing our side lets the\n...\n 249|\n*250|pub fn receive_wan(\n*251|    msg: &WanMessage,\n 252|    origin_node: &str,\n 253|    owlery: &Path,\n 254|    registry: &RegistryHost,\n...\n 506|            let registry = crate::registryhost::RegistryHost::new(\"nobin-node\");\n*507|            let m = WanMessage {\n 508|                target: id.to_string(),\n 509|                from: \"peer\".to_string(),\n 510|                body: \"hello\".to_string(),"}],"details":{"scopePath":"crates/spt/src/cli.rs, crates/spt-daemon/src, crates/spt/src/wansend.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":15,"fileCount":5,"files":["crates/spt/src/cli.rs","crates/spt-daemon/src/dispatch.rs","crates/spt-daemon/src/lib.rs","crates/spt-daemon/src/wan.rs","crates/spt/src/wansend.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":1},{"path":"crates/spt-daemon/src/dispatch.rs","count":2},{"path":"crates/spt-daemon/src/lib.rs","count":1},{"path":"crates/spt-daemon/src/wan.rs","count":6},{"path":"crates/spt/src/wansend.rs","count":5}],"truncated":false,"displayContent":"# crates/\n\n## spt/src/\n### cli.rs\n 6454│#[allow(clippy::too_many_arguments)]\n*6455│fn cmd_send(\n 6456│    target: String,\n 6457│    from: Option<String>,\n 6458│    idle_only: bool,\n### wansend.rs\n   8│//! ([`resolve_across_visible`]: refuse-and-qualify on ambiguity, never a\n  *9│//! guess), and ships one [`WanMessage`] over the broker's QUIC endpoint to\n  10│//! the resolved node. The receiving daemon's dispatcher funnels it through\n  11│//! the access gate + op-id dedup into the target's local delivery\n  12│//! (`spt-daemon::wan::receive_wan`).\n    │...\n  29│};\n *30│use spt_net::net::wanmsg::WanMessage;\n  31│use spt_proto::addr::Address;\n  32│use spt_store::epoch::EpochSource;\n  33│use spt_store::perch;\n    │...\n 207│/// The testable core: resolve against `regs`, dial via `resolver`, write one\n*208│/// `WanMessage` line, finish the stream.\n 209│#[allow(clippy::too_many_arguments)]\n 210│pub fn wan_send_with(\n 211│    brain: &mut Brain,\n    │...\n 255│    };\n*256│    let msg = WanMessage {\n 257│        target: address.id.clone(),\n 258│        from: from.to_string(),\n 259│        body: body.to_string(),\n    │...\n 854│    // [unit->REQ-NET-1] the WAN leg end to end: resolve from a registry map,\n*855│    // dial, ship one WanMessage; B's production dispatcher funnels it into\n 856│    // the target perch's spool. Ambiguity refuses (REQ-INST-10); an\n 857│    // own-node hit degrades to NotFound (the local path's report stands).\n 858│    // [unit->REQ-WAN-SEND-DELIVERY] the honest-SENT decision (#9/#10): EXACTLY the\n\n## spt-daemon/src/\n### dispatch.rs\n  97│    Notif,\n *98│    /// A WAN message feed (untagged `WanMessage` lines).\n  99│    WanMsg,\n 100│    /// A registry replication feed (untagged `RegistryUpdate` lines).\n 101│    Registry,\n    │...\n 627│\n*628│/// WAN message arm: decode `WanMessage` lines, funnel each through the\n 629│/// access-gate + op-id-dedup path, until the peer finishes.\n 630│// [impl->REQ-NET-1]\n 631│fn serve_wan_feed(\n### lib.rs\n 83│//! **D5a (this commit)** puts WAN message delivery on the D4 substrate: a\n*84│//! [`WanMessage`](spt_net::net::wanmsg::WanMessage) NDJSON feed over a\n 85│//! broker-owned QUIC stream funnels through [`wan::receive_wan`] — the ADR-0009\n 86│//! access-gate seam ([`access`], handshake-proven origin only, KNOWN-HAZARDS\n 87│//! 7.5) plus durable op-id dedup (7.2, wire-fed) — into the existing local\n### wan.rs\n   4│//! The sending node resolved the target (D3c policy), dialed, and wrote\n  *5│//! [`WanMessage`] NDJSON lines onto a broker-owned QUIC stream (D4). This\n   6│//! module is the receiving node's funnel from a decoded record to the local\n   7│//! REQ-MSG machinery — it adds exactly two things the local path doesn't have,\n   8│//! then delegates:\n    │...\n  38│use spt_net::net::registry::{node_label_display, Instance};\n *39│use spt_net::net::wanmsg::{WanMessage, WanReplyDecoder};\n  40│\n  41│use crate::brain::{Brain, BrokerEvent};\n  42│use spt_proto::endpoint::EndpointType;\n    │...\n 196│    conn_id: u64,\n*197│    msg: &WanMessage,\n 198│) -> std::io::Result<WanRequestOutcome> {\n 199│    let opened = brain.net_open_stream(conn_id, None)?;\n 200│    // One-shot: the message is everything we send; finishing our side lets the\n    │...\n 249│\n*250│pub fn receive_wan(\n*251│    msg: &WanMessage,\n 252│    origin_node: &str,\n 253│    owlery: &Path,\n 254│    registry: &RegistryHost,\n    │...\n 506│            let registry = crate::registryhost::RegistryHost::new(\"nobin-node\");\n*507│            let m = WanMessage {\n 508│                target: id.to_string(),\n 509│                from: \"peer\".to_string(),\n 510│                body: \"hello\".to_string(),"},"isError":false,"timestamp":1784197467225}}
{"type":"message","id":"57234fa8","parentId":"f864f5ac","timestamp":"2026-07-16T10:24:27.225Z","message":{"role":"toolResult","toolCallId":"call_nlI16nX6FWCyILiGYqeIoEes|fc_017bff223290b7ba016a58b15b181c819ab24caf306d5415f0","toolName":"read","content":[{"type":"text","text":"# Unbound endpoint state + attach-on-session-exists\n\n<!-- [doc->REQ-ENDPOINT-UNBOUND-ATTACH] -->\n\n## Status\n\naccepted (2026-06-23; v0.14.0 endpoint-creation-flow, W6 int keystone green — `multi_subnet_bringup_e2e` proves a fresh hold-unbound endpoint is `spt rc`-attachable pre-bind, and `picker::data::tests::gather_renders_live_unbound` proves the real on-disk→render Unbound seam). Sibling of ADR-0026 (multi-subnet home at creation). Delivered in the endpoint-creation-flow milestone (post-v0.13.2).\n\n## Context\n\n`spt endpoint run`, for an attach/view bringup, waits for the perch to reach `STATUS_ONLINE` — i.e. for the harness to **bind** — before attaching (`await_endpoint_online`, REQ-HAZARD-RC-ATTACH-ONLINE-RACE). That gate was added because attaching too early lost the handshake three ways (empty ring → EOF, the offline status-gate, no live session yet).\n\nBut the broker creates the **session + PTY + OutputLog at spawn — before bind**. Gating the attach on *bind* (perch online) deadlocks a real case: a harness that shows a startup prompt **before** it binds (e.g. Claude Code waiting for the user to clear an initial prompt) never binds until the operator interacts — but the operator can't see or interact, because nothing is attached until bind. Result: the operator stares at a 25s timeout for a prompt they were never shown. The attachable thing (the broker session/PTY) exists well before the gate fires.\n\n## Decision\n\n**Gate the attach on the broker SESSION being attachable, not on the perch being bound/online**, and surface the in-between lifecycle point as a first-class state.\n\n- **Attach-on-session-exists.** `cmd_endpoint_run` (and `spt rc <id>`) attach as soon as a **live broker session** exists for the id — regardless of perch status — so the operator sees and drives the harness immediately, including clearing a bind-gating prompt. This also makes a **headless** bringup attachable before/without bind. The source of truth is the broker `sessions` map (the per-endpoint registry, ADR-0025 W3a). This *preserves* REQ-HAZARD-RC-ATTACH-ONLINE-RACE's \"don't attach before there's a session\" intent — it just moves the gate from the later perch-online point to the earlier session-exists point. Local-only: a remote row can't see another node's broker (renders plain, like the existing controllability limitation).\n\n- **New on-disk status `STATUS_UNBOUND`.** The daemon stamps `UNBOUND` at spawn; `bind` flips it to `ONLINE`; session death → `OFFLINE`. (Chosen over a purely-derived display so the state is explicit and queryable in the daemon-authoritative liveness model.)\n  - **Lifecycle reuses the existing reconcile/exit-waiter** — a dead broker session → `OFFLINE` (the same path that marks dead-but-latched perches offline). No new teardown; `UNBOUND` auto-resolves (→`ONLINE` on bind, →`OFFLINE` on session death).\n  - **`UNBOUND` is attachable but NOT message-addressable** — it has a live PTY but no bound `session_id` yet, so messaging stays gated on `ONLINE`/bound while attach is gated on session-exists.\n\n- **Display.** `EpDisplay` gains an `Unbound` variant. **Amber is already taken** (`HarnessOnly` = online-but-not-broker-controllable — the semantic *opposite* of `Unbound`, which *is* broker-attachable), so `Unbound` uses a **hollow** treatment (the existing filled/hollow square precedent), plus a **controlled-variant** (an `Unbound` endpoint a node is driving) — i.e. hollow vs hollow-controlled, paralleling the filled Online vs Controlled pair.\n\n## Considered Options\n\n- **Keep `await_endpoint_online` + a grace-then-attach-anyway fallback** — a race-prone timer heuristic still anchored on bind, rather than fixing the gate's basis. Rejected.\n- **Derive `Unbound` in the picker only (no on-disk status)** — one source (broker session map), no new liveness status; but the operator chose an explicit on-disk `STATUS_UNBOUND` (queryable, honest in `endpoint list`, not just picker-local). Accepted the extra reconcile/teardown edges that come with it.\n- **Reuse amber for `Unbound`** — semantically wrong (amber = not-controllable; Unbound is attachable) and ambiguous against `HarnessOnly`. Rejected; hollow instead.\n\n## Consequences\n\n- A new on-disk liveness status (`STATUS_UNBOUND`) — the liveness predicates and reconcile/teardown must treat it honestly (attachable, not message-addressable; auto-resolves).\n- `spt rc <id>` works against an unbound endpoint (a live session with no bind) — useful for headless bringups and for clearing a bind-gating prompt.\n- The attach gate moves earlier (session-exists), removing the prompt-before-bind deadlock while keeping the original race protection.\n- `EpDisplay` grows (hollow + hollow-controlled); the picker/legend gain the Unbound state.\n"}],"details":{"displayContent":{"text":"# Unbound endpoint state + attach-on-session-exists\r\n\r\n<!-- [doc->REQ-ENDPOINT-UNBOUND-ATTACH] -->\r\n\r\n## Status\r\n\r\naccepted (2026-06-23; v0.14.0 endpoint-creation-flow, W6 int keystone green — `multi_subnet_bringup_e2e` proves a fresh hold-unbound endpoint is `spt rc`-attachable pre-bind, and `picker::data::tests::gather_renders_live_unbound` proves the real on-disk→render Unbound seam). Sibling of ADR-0026 (multi-subnet home at creation). Delivered in the endpoint-creation-flow milestone (post-v0.13.2).\r\n\r\n## Context\r\n\r\n`spt endpoint run`, for an attach/view bringup, waits for the perch to reach `STATUS_ONLINE` — i.e. for the harness to **bind** — before attaching (`await_endpoint_online`, REQ-HAZARD-RC-ATTACH-ONLINE-RACE). That gate was added because attaching too early lost the handshake three ways (empty ring → EOF, the offline status-gate, no live session yet).\r\n\r\nBut the broker creates the **session + PTY + OutputLog at spawn — before bind**. Gating the attach on *bind* (perch online) deadlocks a real case: a harness that shows a startup prompt **before** it binds (e.g. Claude Code waiting for the user to clear an initial prompt) never binds until the operator interacts — but the operator can't see or interact, because nothing is attached until bind. Result: the operator stares at a 25s timeout for a prompt they were never shown. The attachable thing (the broker session/PTY) exists well before the gate fires.\r\n\r\n## Decision\r\n\r\n**Gate the attach on the broker SESSION being attachable, not on the perch being bound/online**, and surface the in-between lifecycle point as a first-class state.\r\n\r\n- **Attach-on-session-exists.** `cmd_endpoint_run` (and `spt rc <id>`) attach as soon as a **live broker session** exists for the id — regardless of perch status — so the operator sees and drives the harness immediately, including clearing a bind-gating prompt. This also makes a **headless** bringup attachable before/without bind. The source of truth is the broker `sessions` map (the per-endpoint registry, ADR-0025 W3a). This *preserves* REQ-HAZARD-RC-ATTACH-ONLINE-RACE's \"don't attach before there's a session\" intent — it just moves the gate from the later perch-online point to the earlier session-exists point. Local-only: a remote row can't see another node's broker (renders plain, like the existing controllability limitation).\r\n\r\n- **New on-disk status `STATUS_UNBOUND`.** The daemon stamps `UNBOUND` at spawn; `bind` flips it to `ONLINE`; session death → `OFFLINE`. (Chosen over a purely-derived display so the state is explicit and queryable in the daemon-authoritative liveness model.)\r\n  - **Lifecycle reuses the existing reconcile/exit-waiter** — a dead broker session → `OFFLINE` (the same path that marks dead-but-latched perches offline). No new teardown; `UNBOUND` auto-resolves (→`ONLINE` on bind, →`OFFLINE` on session death).\r\n  - **`UNBOUND` is attachable but NOT message-addressable** — it has a live PTY but no bound `session_id` yet, so messaging stays gated on `ONLINE`/bound while attach is gated on session-exists.\r\n\r\n- **Display.** `EpDisplay` gains an `Unbound` variant. **Amber is already taken** (`HarnessOnly` = online-but-not-broker-controllable — the semantic *opposite* of `Unbound`, which *is* broker-attachable), so `Unbound` uses a **hollow** treatment (the existing filled/hollow square precedent), plus a **controlled-variant** (an `Unbound` endpoint a node is driving) — i.e. hollow vs hollow-controlled, paralleling the filled Online vs Controlled pair.\r\n\r\n## Considered Options\r\n\r\n- **Keep `await_endpoint_online` + a grace-then-attach-anyway fallback** — a race-prone timer heuristic still anchored on bind, rather than fixing the gate's basis. Rejected.\r\n- **Derive `Unbound` in the picker only (no on-disk status)** — one source (broker session map), no new liveness status; but the operator chose an explicit on-disk `STATUS_UNBOUND` (queryable, honest in `endpoint list`, not just picker-local). Accepted the extra reconcile/teardown edges that come with it.\r\n- **Reuse amber for `Unbound`** — semantically wrong (amber = not-controllable; Unbound is attachable) and ambiguous against `HarnessOnly`. Rejected; hollow instead.\r\n\r\n## Consequences\r\n\r\n- A new on-disk liveness status (`STATUS_UNBOUND`) — the liveness predicates and reconcile/teardown must treat it honestly (attachable, not message-addressable; auto-resolves).\r\n- `spt rc <id>` works against an unbound endpoint (a live session with no bind) — useful for headless bringups and for clearing a bind-gating prompt.\r\n- The attach gate moves earlier (session-exists), removing the prompt-before-bind deadlock while keeping the original race protection.\r\n- `EpDisplay` grows (hollow + hollow-controlled); the picker/legend gain the Unbound state.\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0027-unbound-endpoint-state-and-attach-on-session.md"}}},"isError":false,"timestamp":1784197467225}}
{"type":"message","id":"600ced9f","parentId":"57234fa8","timestamp":"2026-07-16T10:24:27.225Z","message":{"role":"toolResult","toolCallId":"call_hAQSyumGIpTV77r3N0vLOE7m|fc_017bff223290b7ba016a58b15b1828819a85e2fc33b9098c7f","toolName":"read","content":[{"type":"text","text":"# Activity-gated message delivery + send-modifier axes\n\n<!-- [doc->REQ-MSG-DELIVERY-AXES] -->\n\n## Status\n\naccepted (2026-06-23; design grilled with operator + `doyle`, `/grill-with-docs`). Targets **v0.15.0** (the milestone immediately after v0.14.3). Builds on ADR-0020 (the `<EVENT>` envelope as the sole arriving format), ADR-0022 + its v0.14.2 amendment (translation-binary idle delivery; raw-inject removed), and the activity/idle sentinel already shipped (`api state <busy|idle>`). CONTEXT.md \"activity-gated delivery\" / \"message delivery axes\" / \"message metadata (`json`)\" mint this vocabulary.\n\n## Context\n\nActivity-gated inbound delivery is a **legacy-SPT parity gap that was never written down** (absent from CONTEXT.md and the roadmap). The pieces are half-built: `api state busy|idle` writes the `.idle` sentinel, `delivery::is_idle(id)` reads it, and `resolve_inject_methods(manifest, idle)` maps idle-state → inject methods — but the result is discarded (`let _methods = …`), and `broker::dispatch_endpoint_input` injects unconditionally, its own comment calling activity-gated routing \"a deferred follow wave.\" v0.14.3 (the ADR-0022 amendment) just removed the silent raw-inject fallback, so a missing/failed translation binary now spools loudly — sharpening the question of *when* and *through what* a message should reach an endpoint.\n\nThe operator needs per-message control over that delivery, beyond the single `--deferred` (spool-only, hook-channel) flag that exists today. The risk is modelling these as one flat list of mutually-exclusive \"message types\" — which conflates independent concerns (*when* vs *through what* vs *how long*) and forbids coherent combinations.\n\n## Decision\n\n**Complete the activity-gated delivery model, and expose per-message control as three orthogonal send-modifier axes plus an opaque metadata attribute** — not a flat \"type\" enum.\n\n### Activity-gated delivery (the substrate v0.15.0 wires)\n\nAn inbound message has two possible delivery moments, chosen by the receiver's activity sentinel:\n\n- **active window** — while the endpoint is active, the message spools for the receiver's own hook-poll to drain (non-disruptive).\n- **idle 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.\n\n### Three orthogonal axes (each composes; each defaults to its unrestricted value)\n\n1. **delivery window** (*when*) — mutually exclusive:\n   - **default** — eligible for both windows; delivers in whichever fires first.\n   - `--idle-only` — idle window only; delivered immediately if already idle.\n   - `--active-only` — active window only (hook-poll); never wakes an idle agent. **This is the renamed `--deferred`** (pure rename — the `deferred=1` spool column and the adapter-facing `api poll --include-deferred` keep their names).\n2. **channel restriction** (*through what*) — mutually exclusive, composes with the window:\n   - **unrestricted** — any configured inject method.\n   - `--prefer-native` — the translation binary if one is running, else fall back to the standard methods.\n   - `--force-native` — the translation binary and nothing else (no fallback, no spool-to-another-method).\n   - \"Native\" = the `[message-idle-translation-binary]` PTY channel. The native flags do **not** respect the binary's idle-gating — the **window** flag dictates *when*, the native flag dictates *through what*. So `--force-native --active-only` = the binary injects during the active window, never idle (the InjectFloor already buffers controller input atomically, making mid-turn injection safe).\n3. **persistence** (*how long it waits*) — mutually exclusive, composes:\n   - **durable** (default) — spooled until delivered or TTL.\n   - `--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.\n\n### Metadata (orthogonal to all three axes)\n\n- `--json-payload '<json>'` — attach an opaque JSON metadata block, 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); the receiving adapter (hooks and/or translation binary) parses it. Available to **any** sender (it confers no spt-core authority).\n\n### Hazard reconciliation\n\n`--ephemeral` is the **only** path permitted to drop a message silently — the explicit, sender-opted-in exception to **REQ-HAZARD-IDLE-SILENT-NONDELIVERY** (shipped in v0.14.3). Every non-ephemeral path still spools and reports `delivered=false`. The in-flight hazard gains one clause: *\"…unless the sender set `--ephemeral`.\"*\n\n## Considered Options\n\n- **One flat list of mutually-exclusive \"message types\"** (`--idle-only` / `--active-only` / `--native-only`) — the operator's first framing. Rejected: it conflates an activity *gate* (idle/active) with a *channel* selector (native), forbidding coherent combinations (e.g. binary-during-active) and making `--native-only`'s \"regardless of idle\" impossible to express. Decomposed into the three orthogonal axes above.\n- **Native flags respect the binary's idle-gating** (Reading A) — `--force-native` then only ever fires on idle, making it near-redundant with the default's idle-window channel order and `--prefer-native` meaningless. Rejected for Reading B (native = channel restriction; window owns timing), the only reading where the native flags carry distinct, non-redundant meaning.\n- **`--custom '<json>'` → multiple top-level XML attributes** — rejected in favour of `--json-payload` (single `json` attr). The multi-attr form opens an identity-forgery surface (`{\"from\":\"the-user\"}` becomes a real envelope attr), needs a reserved-set + a future-prefix policy to stay collision-safe, and is *less* expressive (flat strings vs nested/typed JSON). The single-attr blob is collision-proof by construction, simpler for spt-core, and easier for adapters to parse.\n- **Name the immediate-delivery default** (`--now`/`--immediate`) — rejected; once native is its own axis, \"deliver now regardless\" is just the unnamed default. Fewer flags; \"I didn't say when → send it now\" is the least-surprising default.\n- **`--ephemeral` bolted onto `--force-native`** — rejected for a standalone persistence axis: ephemerality is useful beyond native (e.g. `--idle-only --ephemeral` = a wake-up that's pointless if stale), and isolating it gives the hazard carve-out a single clean home.\n"}],"details":{"displayContent":{"text":"# Activity-gated message delivery + send-modifier axes\r\n\r\n<!-- [doc->REQ-MSG-DELIVERY-AXES] -->\r\n\r\n## Status\r\n\r\naccepted (2026-06-23; design grilled with operator + `doyle`, `/grill-with-docs`). Targets **v0.15.0** (the milestone immediately after v0.14.3). Builds on ADR-0020 (the `<EVENT>` envelope as the sole arriving format), ADR-0022 + its v0.14.2 amendment (translation-binary idle delivery; raw-inject removed), and the activity/idle sentinel already shipped (`api state <busy|idle>`). CONTEXT.md \"activity-gated delivery\" / \"message delivery axes\" / \"message metadata (`json`)\" mint this vocabulary.\r\n\r\n## Context\r\n\r\nActivity-gated inbound delivery is a **legacy-SPT parity gap that was never written down** (absent from CONTEXT.md and the roadmap). The pieces are half-built: `api state busy|idle` writes the `.idle` sentinel, `delivery::is_idle(id)` reads it, and `resolve_inject_methods(manifest, idle)` maps idle-state → inject methods — but the result is discarded (`let _methods = …`), and `broker::dispatch_endpoint_input` injects unconditionally, its own comment calling activity-gated routing \"a deferred follow wave.\" v0.14.3 (the ADR-0022 amendment) just removed the silent raw-inject fallback, so a missing/failed translation binary now spools loudly — sharpening the question of *when* and *through what* a message should reach an endpoint.\r\n\r\nThe operator needs per-message control over that delivery, beyond the single `--deferred` (spool-only, hook-channel) flag that exists today. The risk is modelling these as one flat list of mutually-exclusive \"message types\" — which conflates independent concerns (*when* vs *through what* vs *how long*) and forbids coherent combinations.\r\n\r\n## Decision\r\n\r\n**Complete the activity-gated delivery model, and expose per-message control as three orthogonal send-modifier axes plus an opaque metadata attribute** — not a flat \"type\" enum.\r\n\r\n### Activity-gated delivery (the substrate v0.15.0 wires)\r\n\r\nAn inbound message has two possible delivery moments, chosen by the receiver's activity sentinel:\r\n\r\n- **active window** — while the endpoint is active, the message spools for the receiver's own hook-poll to drain (non-disruptive).\r\n- **idle 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.\r\n\r\n### Three orthogonal axes (each composes; each defaults to its unrestricted value)\r\n\r\n1. **delivery window** (*when*) — mutually exclusive:\r\n   - **default** — eligible for both windows; delivers in whichever fires first.\r\n   - `--idle-only` — idle window only; delivered immediately if already idle.\r\n   - `--active-only` — active window only (hook-poll); never wakes an idle agent. **This is the renamed `--deferred`** (pure rename — the `deferred=1` spool column and the adapter-facing `api poll --include-deferred` keep their names).\r\n2. **channel restriction** (*through what*) — mutually exclusive, composes with the window:\r\n   - **unrestricted** — any configured inject method.\r\n   - `--prefer-native` — the translation binary if one is running, else fall back to the standard methods.\r\n   - `--force-native` — the translation binary and nothing else (no fallback, no spool-to-another-method).\r\n   - \"Native\" = the `[message-idle-translation-binary]` PTY channel. The native flags do **not** respect the binary's idle-gating — the **window** flag dictates *when*, the native flag dictates *through what*. So `--force-native --active-only` = the binary injects during the active window, never idle (the InjectFloor already buffers controller input atomically, making mid-turn injection safe).\r\n3. **persistence** (*how long it waits*) — mutually exclusive, composes:\r\n   - **durable** (default) — spooled until delivered or TTL.\r\n   - `--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.\r\n\r\n### Metadata (orthogonal to all three axes)\r\n\r\n- `--json-payload '<json>'` — attach an opaque JSON metadata block, 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); the receiving adapter (hooks and/or translation binary) parses it. Available to **any** sender (it confers no spt-core authority).\r\n\r\n### Hazard reconciliation\r\n\r\n`--ephemeral` is the **only** path permitted to drop a message silently — the explicit, sender-opted-in exception to **REQ-HAZARD-IDLE-SILENT-NONDELIVERY** (shipped in v0.14.3). Every non-ephemeral path still spools and reports `delivered=false`. The in-flight hazard gains one clause: *\"…unless the sender set `--ephemeral`.\"*\r\n\r\n## Considered Options\r\n\r\n- **One flat list of mutually-exclusive \"message types\"** (`--idle-only` / `--active-only` / `--native-only`) — the operator's first framing. Rejected: it conflates an activity *gate* (idle/active) with a *channel* selector (native), forbidding coherent combinations (e.g. binary-during-active) and making `--native-only`'s \"regardless of idle\" impossible to express. Decomposed into the three orthogonal axes above.\r\n- **Native flags respect the binary's idle-gating** (Reading A) — `--force-native` then only ever fires on idle, making it near-redundant with the default's idle-window channel order and `--prefer-native` meaningless. Rejected for Reading B (native = channel restriction; window owns timing), the only reading where the native flags carry distinct, non-redundant meaning.\r\n- **`--custom '<json>'` → multiple top-level XML attributes** — rejected in favour of `--json-payload` (single `json` attr). The multi-attr form opens an identity-forgery surface (`{\"from\":\"the-user\"}` becomes a real envelope attr), needs a reserved-set + a future-prefix policy to stay collision-safe, and is *less* expressive (flat strings vs nested/typed JSON). The single-attr blob is collision-proof by construction, simpler for spt-core, and easier for adapters to parse.\r\n- **Name the immediate-delivery default** (`--now`/`--immediate`) — rejected; once native is its own axis, \"deliver now regardless\" is just the unnamed default. Fewer flags; \"I didn't say when → send it now\" is the least-surprising default.\r\n- **`--ephemeral` bolted onto `--force-native`** — rejected for a standalone persistence axis: ephemerality is useful beyond native (e.g. `--idle-only --ephemeral` = a wake-up that's pointless if stale), and isolating it gives the hazard carve-out a single clean home.\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0028-activity-gated-delivery-and-send-modifier-axes.md"}}},"isError":false,"timestamp":1784197467225}}
{"type":"message","id":"2f57ce11","parentId":"600ced9f","timestamp":"2026-07-16T10:24:27.236Z","message":{"role":"toolResult","toolCallId":"call_hkIpJRrLAWwqleUbR0LkwNAc|fc_017bff223290b7ba016a58b15b1810819a89664669eaed0b49","toolName":"read","content":[{"type":"text","text":"# ADR-0022 — spt-hosted idle message delivery via an adapter translation binary\n\n- Status: Accepted (design grilled 2026-06-19; supersedes the v0.11.0 hardcoded direct-PTY-inject). **Amended 2026-06-23 (v0.14.2) — raw-inject removed entirely; see \"Amendment\" below.**\n- Milestone: v0.13.0 (amendment: v0.14.2)\n\n## Context\n\nAn spt-hosted endpoint is one whose PTY the daemon owns (terminal-wrapper / GUI topology). Inbound\ninter-agent messages must reach the agent running inside that PTY while it is **idle**.\n\nThe v0.11.0 implementation hardcodes a single delivery method: the broker types the message payload\nplus `\\r` straight into the PTY stdin (`dispatch_endpoint_input` → `session.write_input`). This has\ntwo fatal problems:\n\n1. **It is harness-blind.** Real TUIs need input that *precedes* and *follows* the payload — e.g.\n   Claude Code needs `ctrl+s` to stash any in-progress user text, then the payload, then `ctrl+s`\n   to restore it. A bare `payload + \\r` corrupts the user's in-progress input and may mis-fire.\n2. **It fights the controller.** When an operator is attached via `spt rc`, the raw inject competes\n   with the operator on the same PTY and the broker's single-threaded I/O path; the operator loses\n   control and the broker can wedge (the v0.12.1 \"attach wedge\", returning via this trigger).\n\nToday there is also a coverage gap: `api bind` registers no listener port, so a listener-less\nspt-hosted perch *spools* inbound messages — only spooling + adapter-poll works.\n\nWe previously locked (Q1 → \"A+\", CCS session 16531b26, 2026-06-17) a **unified** model: the daemon's\npoll feed is the one idle substrate for both topologies; the consumer differs. This ADR completes\nthat design.\n\n## Decision\n\nIntroduce an opt-in adapter-supplied **translation binary**, declared by the manifest section\n**`[message-idle-translation-binary]`** — a **table carrying a `path` scalar** (modeled as a table,\nnot a bare top-level scalar, so a section that precedes it cannot silently absorb the key, and so\nit stays N+1 extensible; spt-core does **not** `deny_unknown_fields` here, so a future key against an\nolder spt-core degrades gracefully rather than hard-failing the manifest). It is a **pure\nstdin→stdout filter**; **spt-core owns the binary's lifecycle and every PTY write.**\n\n- **Lifecycle:** spt-core spawns the binary when the endpoint comes up and terminates it when the\n  endpoint goes down.\n- **stdin (spt-core → binary), JSON-lines:**\n  - `{type:\"init\", endpoint_id, node}` — first line; identity.\n  - `{type:\"event\", envelope:\"<EVENT type=\\\"msg\\\" …>\"}` — each inbound message (ADR-0020 envelope).\n  - `{type:\"input\"}` — a content-free ping each time the operator types, so the binary can track\n    user-idle state and roll its own idle-gated buffering. (PTY input content is NOT duplicated.)\n- **stdout (binary → spt-core), JSON-lines:** keystroke-commands — `{key:\"ctrl+s\"}`,\n  `{delay_ms:50}`, `{text:\"<payload>\"}`, `{key:\"enter\"}`, … (extensible).\n- **spt-core applies the emitted sequence to the broker PTY ATOMICALLY:** controller keystrokes\n  arriving mid-sequence are BUFFERED and flushed after, so a stash/restore can never be clobbered.\n  spt-core owning every PTY write is what lets injection coexist with a live `spt rc` controller.\n- **Poll-listener preference:** spt-core prefers a perch's poll listener if one exists, so an\n  spt-hosted endpoint can run a listener AND keep `spt rc`. A dev may keep the perch \"idle-ready\"\n  and use either the listener or the translation binary for *all* delivery.\n- **Scope:** idle delivery only. Busy/mid-turn delivery stays adapter hook-injection.\n\nThe v0.11.0 raw inject is the **degenerate special case** — a translation binary that emits\n`{text:payload}{key:enter}` with no choreography.\n\n## Consequences\n\n- spt-claude-code ships a ~30-line translation binary: read an `event` on stdin, emit\n  `ctrl+s → delay 50 → text(payload) → enter → delay 50 → ctrl+s` on stdout. Perri builds it\n  against this contract; the choreography lives entirely in the adapter's binary.\n- The control-loss / broker-wedge bug is fixed at the source it shares with this feature: the\n  atomic PTY-write coordination (v0.13.0 W1) makes ANY injection (raw today, choreographed\n  tomorrow) coexist with the controller. This ADR and that fix land in step.\n- CONTEXT.md:39 reworded: spt-hosted needs no *separate harness-owned* relay — the daemon-side\n  translation binary is the consumer of the same poll feed.\n- New manifest primitive (not collapsed into `[inject]{Pty,Hook,Relay,Http}` / `notif_command`),\n  but it shares the poll-feed substrate with notif delivery.\n\n## Author-time proof: `spt adapter translate-proof`\n\n<!-- [doc->REQ-ADAPTER-TRANSLATE-PROOF] -->\n\nThe translation binary is opaque to spt-core and runs deep inside the idle-delivery path (broker →\nPTY), so a broken one is hard to diagnose from a live session. `spt adapter translate-proof\n<adapter> --event <envelope> [--session <id>]` is the **author-time proof** for it — symmetric to\n`spt adapter digest-proof` for the `[digest]` extractor.\n\nIt runs the **same driver the daemon runs** (`spt_daemon::translation`), verbatim — no protocol\nreimplementation. It:\n\n1. resolves the adapter's declared `[message-idle-translation-binary].path` against the install dir\n   (REQ-INSTALL-11), exactly as the daemon does at bringup;\n2. spawns it with `TranslationChild::spawn` and sends the identity `{type:\"init\", endpoint_id, node}`\n   line (`endpoint_id` = the adapter option, `node` = this host's label), then the inbound\n   `{type:\"event\", envelope}` line — feeding the `--event` envelope verbatim after filling `{id}`\n   (→ the option) and `{session_id}` (→ `--session`, else a placeholder), the same keys the daemon\n   fills into a delivered envelope;\n3. drains the emitted `{key}`/`{text}`/`{delay_ms}`/`{commit}` keystroke-command stream (until the\n   terminating `{commit}` or a short overall budget) and prints it author-readable — each `{key}`\n   rendered with its `key_to_bytes` PTY bytes — so the author sees exactly what the binary would\n   inject.\n\n**It is the EMIT half only.** It proves the binary's spawn → feed → emit contract; it does **not**\nexercise spt-core's atomic PTY apply or the controller-input buffering (that coordination is covered\nby the W2 inject-control gate). `--help` states this. The `TranslationChild` `Drop` does the bounded\nno-zombie reap, so the proof never leaks the child.\n\nExit codes mirror digest-proof: **0** ok; **1** on spawn-fail / zero commands / no terminating\n`{commit}` / unparseable output (a binary that emits a sequence the broker could never commit would\nhit the commit deadline and FAULT live — the proof catches it first, never reading as a silent\nsuccess); **2** when the adapter declares no `[message-idle-translation-binary]` section.\n\n## Alternatives considered\n\n- **Manifest-declared keystroke sequence (no binary).** Rejected: cannot branch on message content\n  or user-idle state; the binary form is barely more work and far more capable.\n- **Binary hosts its own `spt api listen` + writes the PTY directly.** Rejected: spt-core already\n  owns listener logic, and if the binary wrote the PTY directly spt-core could not coordinate\n  atomicity with the controller. Piping the feed to stdin and reading keystrokes from stdout keeps\n  the binary trivial and keeps every PTY write under spt-core's control.\n- **Keep two distinct delivery paths (Q1 option B).** Rejected in favour of the unified poll-feed\n  substrate.\n\n## Amendment (2026-06-23, v0.14.2) — raw-inject removed entirely\n\n**Operator ruling (2026-06-23):** idle message delivery is exactly **two paths** — harness-hosted\n**relay-poll**, or spt-hosted **translation binary** — with **nothing in between**. The v0.11.0\n**raw PTY inject is removed as a delivery path**, superseding the original Decision's framing of it as\nthe \"degenerate special case\" (this doc, §Decision last line).\n\n- **spt-hosted idle delivery is translation-binary-ONLY.** An adapter that declares no\n…\n  raw-injected.\n…\n\n…\n\n…\nnow doubles as the author-time check for this hard rule.\n\n[Showing lines 1-123 of 207. Use :124 to continue]"}],"details":{"truncation":{"content":"# ADR-0022 — spt-hosted idle message delivery via an adapter translation binary\r\n\r\n- Status: Accepted (design grilled 2026-06-19; supersedes the v0.11.0 hardcoded direct-PTY-inject). **Amended 2026-06-23 (v0.14.2) — raw-inject removed entirely; see \"Amendment\" below.**\r\n- Milestone: v0.13.0 (amendment: v0.14.2)\r\n\r\n## Context\r\n\r\nAn spt-hosted endpoint is one whose PTY the daemon owns (terminal-wrapper / GUI topology). Inbound\r\ninter-agent messages must reach the agent running inside that PTY while it is **idle**.\r\n\r\nThe v0.11.0 implementation hardcodes a single delivery method: the broker types the message payload\r\nplus `\\r` straight into the PTY stdin (`dispatch_endpoint_input` → `session.write_input`). This has\r\ntwo fatal problems:\r\n\r\n1. **It is harness-blind.** Real TUIs need input that *precedes* and *follows* the payload — e.g.\r\n   Claude Code needs `ctrl+s` to stash any in-progress user text, then the payload, then `ctrl+s`\r\n   to restore it. A bare `payload + \\r` corrupts the user's in-progress input and may mis-fire.\r\n2. **It fights the controller.** When an operator is attached via `spt rc`, the raw inject competes\r\n   with the operator on the same PTY and the broker's single-threaded I/O path; the operator loses\r\n   control and the broker can wedge (the v0.12.1 \"attach wedge\", returning via this trigger).\r\n\r\nToday there is also a coverage gap: `api bind` registers no listener port, so a listener-less\r\nspt-hosted perch *spools* inbound messages — only spooling + adapter-poll works.\r\n\r\nWe previously locked (Q1 → \"A+\", CCS session 16531b26, 2026-06-17) a **unified** model: the daemon's\r\npoll feed is the one idle substrate for both topologies; the consumer differs. This ADR completes\r\nthat design.\r\n\r\n## Decision\r\n\r\nIntroduce an opt-in adapter-supplied **translation binary**, declared by the manifest section\r\n**`[message-idle-translation-binary]`** — a **table carrying a `path` scalar** (modeled as a table,\r\nnot a bare top-level scalar, so a section that precedes it cannot silently absorb the key, and so\r\nit stays N+1 extensible; spt-core does **not** `deny_unknown_fields` here, so a future key against an\r\nolder spt-core degrades gracefully rather than hard-failing the manifest). It is a **pure\r\nstdin→stdout filter**; **spt-core owns the binary's lifecycle and every PTY write.**\r\n\r\n- **Lifecycle:** spt-core spawns the binary when the endpoint comes up and terminates it when the\r\n  endpoint goes down.\r\n- **stdin (spt-core → binary), JSON-lines:**\r\n  - `{type:\"init\", endpoint_id, node}` — first line; identity.\r\n  - `{type:\"event\", envelope:\"<EVENT type=\\\"msg\\\" …>\"}` — each inbound message (ADR-0020 envelope).\r\n  - `{type:\"input\"}` — a content-free ping each time the operator types, so the binary can track\r\n    user-idle state and roll its own idle-gated buffering. (PTY input content is NOT duplicated.)\r\n- **stdout (binary → spt-core), JSON-lines:** keystroke-commands — `{key:\"ctrl+s\"}`,\r\n  `{delay_ms:50}`, `{text:\"<payload>\"}`, `{key:\"enter\"}`, … (extensible).\r\n- **spt-core applies the emitted sequence to the broker PTY ATOMICALLY:** controller keystrokes\r\n  arriving mid-sequence are BUFFERED and flushed after, so a stash/restore can never be clobbered.\r\n  spt-core owning every PTY write is what lets injection coexist with a live `spt rc` controller.\r\n- **Poll-listener preference:** spt-core prefers a perch's poll listener if one exists, so an\r\n  spt-hosted endpoint can run a listener AND keep `spt rc`. A dev may keep the perch \"idle-ready\"\r\n  and use either the listener or the translation binary for *all* delivery.\r\n- **Scope:** idle delivery only. Busy/mid-turn delivery stays adapter hook-injection.\r\n\r\nThe v0.11.0 raw inject is the **degenerate special case** — a translation binary that emits\r\n`{text:payload}{key:enter}` with no choreography.\r\n\r\n## Consequences\r\n\r\n- spt-claude-code ships a ~30-line translation binary: read an `event` on stdin, emit\r\n  `ctrl+s → delay 50 → text(payload) → enter → delay 50 → ctrl+s` on stdout. Perri builds it\r\n  against this contract; the choreography lives entirely in the adapter's binary.\r\n- The control-loss / broker-wedge bug is fixed at the source it shares with this feature: the\r\n  atomic PTY-write coordination (v0.13.0 W1) makes ANY injection (raw today, choreographed\r\n  tomorrow) coexist with the controller. This ADR and that fix land in step.\r\n- CONTEXT.md:39 reworded: spt-hosted needs no *separate harness-owned* relay — the daemon-side\r\n  translation binary is the consumer of the same poll feed.\r\n- New manifest primitive (not collapsed into `[inject]{Pty,Hook,Relay,Http}` / `notif_command`),\r\n  but it shares the poll-feed substrate with notif delivery.\r\n\r\n## Author-time proof: `spt adapter translate-proof`\r\n\r\n<!-- [doc->REQ-ADAPTER-TRANSLATE-PROOF] -->\r\n\r\nThe translation binary is opaque to spt-core and runs deep inside the idle-delivery path (broker →\r\nPTY), so a broken one is hard to diagnose from a live session. `spt adapter translate-proof\r\n<adapter> --event <envelope> [--session <id>]` is the **author-time proof** for it — symmetric to\r\n`spt adapter digest-proof` for the `[digest]` extractor.\r\n\r\nIt runs the **same driver the daemon runs** (`spt_daemon::translation`), verbatim — no protocol\r\nreimplementation. It:\r\n\r\n1. resolves the adapter's declared `[message-idle-translation-binary].path` against the install dir\r\n   (REQ-INSTALL-11), exactly as the daemon does at bringup;\r\n2. spawns it with `TranslationChild::spawn` and sends the identity `{type:\"init\", endpoint_id, node}`\r\n   line (`endpoint_id` = the adapter option, `node` = this host's label), then the inbound\r\n   `{type:\"event\", envelope}` line — feeding the `--event` envelope verbatim after filling `{id}`\r\n   (→ the option) and `{session_id}` (→ `--session`, else a placeholder), the same keys the daemon\r\n   fills into a delivered envelope;\r\n3. drains the emitted `{key}`/`{text}`/`{delay_ms}`/`{commit}` keystroke-command stream (until the\r\n   terminating `{commit}` or a short overall budget) and prints it author-readable — each `{key}`\r\n   rendered with its `key_to_bytes` PTY bytes — so the author sees exactly what the binary would\r\n   inject.\r\n\r\n**It is the EMIT half only.** It proves the binary's spawn → feed → emit contract; it does **not**\r\nexercise spt-core's atomic PTY apply or the controller-input buffering (that coordination is covered\r\nby the W2 inject-control gate). `--help` states this. The `TranslationChild` `Drop` does the bounded\r\nno-zombie reap, so the proof never leaks the child.\r\n\r\nExit codes mirror digest-proof: **0** ok; **1** on spawn-fail / zero commands / no terminating\r\n`{commit}` / unparseable output (a binary that emits a sequence the broker could never commit would\r\nhit the commit deadline and FAULT live — the proof catches it first, never reading as a silent\r\nsuccess); **2** when the adapter declares no `[message-idle-translation-binary]` section.\r\n\r\n## Alternatives considered\r\n\r\n- **Manifest-declared keystroke sequence (no binary).** Rejected: cannot branch on message content\r\n  or user-idle state; the binary form is barely more work and far more capable.\r\n- **Binary hosts its own `spt api listen` + writes the PTY directly.** Rejected: spt-core already\r\n  owns listener logic, and if the binary wrote the PTY directly spt-core could not coordinate\r\n  atomicity with the controller. Piping the feed to stdin and reading keystrokes from stdout keeps\r\n  the binary trivial and keeps every PTY write under spt-core's control.\r\n- **Keep two distinct delivery paths (Q1 option B).** Rejected in favour of the unified poll-feed\r\n  substrate.\r\n\r\n## Amendment (2026-06-23, v0.14.2) — raw-inject removed entirely\r\n\r\n**Operator ruling (2026-06-23):** idle message delivery is exactly **two paths** — harness-hosted\r\n**relay-poll**, or spt-hosted **translation binary** — with **nothing in between**. The v0.11.0\r\n**raw PTY inject is removed as a delivery path**, superseding the original Decision's framing of it as\r\nthe \"degenerate special case\" (this doc, §Decision last line).\r\n\r\n- **spt-hosted idle delivery is translation-binary-ONLY.** An adapter that declares no\r","truncated":true,"truncatedBy":"lines","totalLines":207,"totalBytes":8202,"outputLines":123,"outputBytes":8202,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"# ADR-0022 — spt-hosted idle message delivery via an adapter translation binary\r\n\r\n- Status: Accepted (design grilled 2026-06-19; supersedes the v0.11.0 hardcoded direct-PTY-inject). **Amended 2026-06-23 (v0.14.2) — raw-inject removed entirely; see \"Amendment\" below.**\r\n- Milestone: v0.13.0 (amendment: v0.14.2)\r\n\r\n## Context\r\n\r\nAn spt-hosted endpoint is one whose PTY the daemon owns (terminal-wrapper / GUI topology). Inbound\r\ninter-agent messages must reach the agent running inside that PTY while it is **idle**.\r\n\r\nThe v0.11.0 implementation hardcodes a single delivery method: the broker types the message payload\r\nplus `\\r` straight into the PTY stdin (`dispatch_endpoint_input` → `session.write_input`). This has\r\ntwo fatal problems:\r\n\r\n1. **It is harness-blind.** Real TUIs need input that *precedes* and *follows* the payload — e.g.\r\n   Claude Code needs `ctrl+s` to stash any in-progress user text, then the payload, then `ctrl+s`\r\n   to restore it. A bare `payload + \\r` corrupts the user's in-progress input and may mis-fire.\r\n2. **It fights the controller.** When an operator is attached via `spt rc`, the raw inject competes\r\n   with the operator on the same PTY and the broker's single-threaded I/O path; the operator loses\r\n   control and the broker can wedge (the v0.12.1 \"attach wedge\", returning via this trigger).\r\n\r\nToday there is also a coverage gap: `api bind` registers no listener port, so a listener-less\r\nspt-hosted perch *spools* inbound messages — only spooling + adapter-poll works.\r\n\r\nWe previously locked (Q1 → \"A+\", CCS session 16531b26, 2026-06-17) a **unified** model: the daemon's\r\npoll feed is the one idle substrate for both topologies; the consumer differs. This ADR completes\r\nthat design.\r\n\r\n## Decision\r\n\r\nIntroduce an opt-in adapter-supplied **translation binary**, declared by the manifest section\r\n**`[message-idle-translation-binary]`** — a **table carrying a `path` scalar** (modeled as a table,\r\nnot a bare top-level scalar, so a section that precedes it cannot silently absorb the key, and so\r\nit stays N+1 extensible; spt-core does **not** `deny_unknown_fields` here, so a future key against an\r\nolder spt-core degrades gracefully rather than hard-failing the manifest). It is a **pure\r\nstdin→stdout filter**; **spt-core owns the binary's lifecycle and every PTY write.**\r\n\r\n- **Lifecycle:** spt-core spawns the binary when the endpoint comes up and terminates it when the\r\n  endpoint goes down.\r\n- **stdin (spt-core → binary), JSON-lines:**\r\n  - `{type:\"init\", endpoint_id, node}` — first line; identity.\r\n  - `{type:\"event\", envelope:\"<EVENT type=\\\"msg\\\" …>\"}` — each inbound message (ADR-0020 envelope).\r\n  - `{type:\"input\"}` — a content-free ping each time the operator types, so the binary can track\r\n    user-idle state and roll its own idle-gated buffering. (PTY input content is NOT duplicated.)\r\n- **stdout (binary → spt-core), JSON-lines:** keystroke-commands — `{key:\"ctrl+s\"}`,\r\n  `{delay_ms:50}`, `{text:\"<payload>\"}`, `{key:\"enter\"}`, … (extensible).\r\n- **spt-core applies the emitted sequence to the broker PTY ATOMICALLY:** controller keystrokes\r\n  arriving mid-sequence are BUFFERED and flushed after, so a stash/restore can never be clobbered.\r\n  spt-core owning every PTY write is what lets injection coexist with a live `spt rc` controller.\r\n- **Poll-listener preference:** spt-core prefers a perch's poll listener if one exists, so an\r\n  spt-hosted endpoint can run a listener AND keep `spt rc`. A dev may keep the perch \"idle-ready\"\r\n  and use either the listener or the translation binary for *all* delivery.\r\n- **Scope:** idle delivery only. Busy/mid-turn delivery stays adapter hook-injection.\r\n\r\nThe v0.11.0 raw inject is the **degenerate special case** — a translation binary that emits\r\n`{text:payload}{key:enter}` with no choreography.\r\n\r\n## Consequences\r\n\r\n- spt-claude-code ships a ~30-line translation binary: read an `event` on stdin, emit\r\n  `ctrl+s → delay 50 → text(payload) → enter → delay 50 → ctrl+s` on stdout. Perri builds it\r\n  against this contract; the choreography lives entirely in the adapter's binary.\r\n- The control-loss / broker-wedge bug is fixed at the source it shares with this feature: the\r\n  atomic PTY-write coordination (v0.13.0 W1) makes ANY injection (raw today, choreographed\r\n  tomorrow) coexist with the controller. This ADR and that fix land in step.\r\n- CONTEXT.md:39 reworded: spt-hosted needs no *separate harness-owned* relay — the daemon-side\r\n  translation binary is the consumer of the same poll feed.\r\n- New manifest primitive (not collapsed into `[inject]{Pty,Hook,Relay,Http}` / `notif_command`),\r\n  but it shares the poll-feed substrate with notif delivery.\r\n\r\n## Author-time proof: `spt adapter translate-proof`\r\n\r\n<!-- [doc->REQ-ADAPTER-TRANSLATE-PROOF] -->\r\n\r\nThe translation binary is opaque to spt-core and runs deep inside the idle-delivery path (broker →\r\nPTY), so a broken one is hard to diagnose from a live session. `spt adapter translate-proof\r\n<adapter> --event <envelope> [--session <id>]` is the **author-time proof** for it — symmetric to\r\n`spt adapter digest-proof` for the `[digest]` extractor.\r\n\r\nIt runs the **same driver the daemon runs** (`spt_daemon::translation`), verbatim — no protocol\r\nreimplementation. It:\r\n\r\n1. resolves the adapter's declared `[message-idle-translation-binary].path` against the install dir\r\n   (REQ-INSTALL-11), exactly as the daemon does at bringup;\r\n2. spawns it with `TranslationChild::spawn` and sends the identity `{type:\"init\", endpoint_id, node}`\r\n   line (`endpoint_id` = the adapter option, `node` = this host's label), then the inbound\r\n   `{type:\"event\", envelope}` line — feeding the `--event` envelope verbatim after filling `{id}`\r\n   (→ the option) and `{session_id}` (→ `--session`, else a placeholder), the same keys the daemon\r\n   fills into a delivered envelope;\r\n3. drains the emitted `{key}`/`{text}`/`{delay_ms}`/`{commit}` keystroke-command stream (until the\r\n   terminating `{commit}` or a short overall budget) and prints it author-readable — each `{key}`\r\n   rendered with its `key_to_bytes` PTY bytes — so the author sees exactly what the binary would\r\n   inject.\r\n\r\n**It is the EMIT half only.** It proves the binary's spawn → feed → emit contract; it does **not**\r\nexercise spt-core's atomic PTY apply or the controller-input buffering (that coordination is covered\r\nby the W2 inject-control gate). `--help` states this. The `TranslationChild` `Drop` does the bounded\r\nno-zombie reap, so the proof never leaks the child.\r\n\r\nExit codes mirror digest-proof: **0** ok; **1** on spawn-fail / zero commands / no terminating\r\n`{commit}` / unparseable output (a binary that emits a sequence the broker could never commit would\r\nhit the commit deadline and FAULT live — the proof catches it first, never reading as a silent\r\nsuccess); **2** when the adapter declares no `[message-idle-translation-binary]` section.\r\n\r\n## Alternatives considered\r\n\r\n- **Manifest-declared keystroke sequence (no binary).** Rejected: cannot branch on message content\r\n  or user-idle state; the binary form is barely more work and far more capable.\r\n- **Binary hosts its own `spt api listen` + writes the PTY directly.** Rejected: spt-core already\r\n  owns listener logic, and if the binary wrote the PTY directly spt-core could not coordinate\r\n  atomicity with the controller. Piping the feed to stdin and reading keystrokes from stdout keeps\r\n  the binary trivial and keeps every PTY write under spt-core's control.\r\n- **Keep two distinct delivery paths (Q1 option B).** Rejected in favour of the unified poll-feed\r\n  substrate.\r\n\r\n## Amendment (2026-06-23, v0.14.2) — raw-inject removed entirely\r\n\r\n**Operator ruling (2026-06-23):** idle message delivery is exactly **two paths** — harness-hosted\r\n**relay-poll**, or spt-hosted **translation binary** — with **nothing in between**. The v0.11.0\r\n**raw PTY inject is removed as a delivery path**, superseding the original Decision's framing of it as\r\nthe \"degenerate special case\" (this doc, §Decision last line).\r\n\r\n- **spt-hosted idle delivery is translation-binary-ONLY.** An adapter that declares no\r\n…\n  raw-injected.\n…\n\n…\n\n…\nnow doubles as the author-time check for this hard rule.","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,null,128,null,134,null,151,null,206]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\adr\\0022-spt-hosted-idle-delivery-translation-binary.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":207,"totalBytes":8202,"outputLines":123,"outputBytes":8202,"shownRange":{"start":1,"end":123},"nextOffset":124}}},"isError":false,"timestamp":1784197467236}}
{"type":"message","id":"a6170328","parentId":"2f57ce11","timestamp":"2026-07-16T10:24:27.237Z","message":{"role":"toolResult","toolCallId":"call_zdP9qiBuGOXkwkEm58ypkWat|fc_017bff223290b7ba016a58b15b17f4819a82272ba706bffc15","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## registryhost.rs\n 13|//!   KNOWN-HAZARDS 7.5);\n*14|//! - **local**: [`advertise_local`](RegistryHost::advertise_local) scans the\n 15|//!   owlery and (re-)advertises every visible self perch with a fresh\n 16|//!   [`EpochSource`] epoch — the per-node monotonic lease key, never\n 17|//!   wall-clock (KNOWN-HAZARDS 4.8).\n...\n 344|    // [impl->REQ-INST-7]\n*345|    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {\n 346|        let subnets = SubnetStore::load();\n 347|        let mut vis = VisibilityStore::load();\n 348|        // First-join home adoption (D9-5, REQ-INST-15): within one\n...\n 364|        // broadcast=subnet ride every row this node authors — the other-node\n*365|        // discovery arm. Computed once per scan (the registration set is\n 366|        // node-level).\n 367|        // [impl->REQ-EP-5]\n 368|        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())\n...\n 425|            // identically instead of reducing every remote row to plain green.\n*426|            //  - bound: a bound live broker session (`is_perch_alive` is bound-gated,\n 427|            //    so an UNBOUND perch reads false — exactly the fill duality the\n 428|            //    picker keys on).\n 429|            //  - controller_node: the broker-owned `driven_by` (the driving node),\n 430|            //    if any → the remote blue \"controlled\" + `controlled by <node>` pin.\n*431|            //  - harness_only: a LiveAgent with no broker PTY seat — mirrors the\n*432|            //    picker's `controllable != Some(true)` for a `live_agent` (a legacy\n 433|            //    None self-corrects on the next bind, so it advertises harness-only\n 434|            //    until then, matching the local amber).\n 435|            // [impl->REQ-SUBNET-DISPLAY-PARITY]\n...\n 448|            );\n*449|            let harness_only = info.as_ref().is_some_and(|i| {\n*450|                i.state == \"live_agent\" && i.controllable != Some(true)\n 451|            });\n 452|            // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS): the endpoint's\n 453|            // real harness adapter + its recent projects + the EXPLICIT any-controller\n...\n 466|                let Ok(ep) = epoch.next_epoch() else { continue };\n*467|                let instance = Instance {\n 468|                    node: self.node_hex.clone(),\n 469|                    status,\n 470|                    epoch: ep,\n...\n 492|                    controller_node: controller_node.clone(),\n*493|                    harness_only,\n 494|                    // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS).\n 495|                    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n 496|                    adapter: adapter.clone(),\n...\n 526|        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\n*527|        // this scan only refreshes perches that still exist, so the stale row is\n 528|        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\n 529|        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\n 530|        // epoch: the lease orders it, so it stops showing Active locally\n...\n 548|                let Ok(ep) = epoch.next_epoch() else { continue };\n*549|                let instance = Instance {\n 550|                    node: self.node_hex.clone(),\n 551|                    status: Status::Offline,\n 552|                    epoch: ep,\n...\n 562|                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\n*563|                    bound: false,\n 564|                    controller_node: None,\n*565|                    harness_only: false,\n 566|                    adapter: None,\n 567|                    recent_projects: Vec::new(),\n 568|                    controlled: false,\n...\n 614|    /// map). A row whose author has NO heard stamp is treated as silent —\n*615|    /// rows only enter via admitted feeds (which stamp) or `advertise_local`\n 616|    /// (own rows, excluded), so an unstamped foreign row is already an\n 617|    /// anomaly and fails toward eviction. A merely-offline peer reconverges\n 618|    /// within one pump cadence of its return (re-advertisement from its\n...\n 797|/// The [`Status`] this node advertises for one of its OWN perches (the\n*798|/// self-gossip path of [`RegistryHost::advertise_local`]).\n 799|///\n 800|/// - **Bound + alive** (`is_perch_alive`): follow the resting-state machine —\n 801|///   Active / Dormant / Suspended per the perch's resting record (recordless ⇒\n...\n 1012|/// start/stop, a rest edge) drops it; the pump consumes it and forces a\n*1013|/// registry round NOW (advertise_local + peer push) instead of waiting out\n 1014|/// the cadence. A WAKE of the existing loop, never a second advertisement\n 1015|/// path: the forced round takes the exact same code path, so the epoch lease\n 1016|/// and visibility gates ride unchanged. File-based because the transitions\n...\n 1141|            endpoint_id: id.to_string(),\n*1142|            instance: Instance {\n 1143|                node: node.to_string(),\n 1144|                status: Status::Active,\n 1145|                epoch,\n...\n 1151|                endpoint_type: None,\n*1152|                bound: true,\n 1153|                controller_node: None,\n*1154|                harness_only: false,\n 1155|                adapter: None,\n 1156|                recent_projects: Vec::new(),\n 1157|                controlled: false,\n## access.rs\n 117|pub fn classify_inbound(endpoint: &str, origin_node: &str, now_ms: u64) -> InboundClass {\n*118|    if RecentOutbound::load().correlates(endpoint, origin_node, now_ms, REPLY_WINDOW_MS) {\n 119|        InboundClass::Reply\n 120|    } else {\n 121|        InboundClass::Unsolicited\n...\n 131|pub fn note_outbound(endpoint: &str, node: &str, now_ms: u64) {\n*132|    let mut log = RecentOutbound::load();\n 133|    log.record(endpoint, node, now_ms, REPLY_WINDOW_MS);\n 134|    let _ = log.save();\n 135|}\n## brain.rs\n 1691|                Some(dl) => {\n*1692|                    // Total-wait bound: each recv is the REMAINING budget to the\n 1693|                    // call's entry deadline, so unrelated frames dripping in\n 1694|                    // cannot reset it (the per-read-timeout wedge the ruling\n 1695|                    // forbids). A non-positive remaining → recv_timeout(ZERO),\n## conn.rs\n 170|    /// The absolute per-write bound (gate-wait + OS write completion).\n*171|    bound: Duration,\n 172|    /// The raw OS handle/fd of the underlying stream, captured at\n 173|    /// construction for the out-of-band abort. The recv and send halves share\n 174|    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\n...\n 425|    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n*426|    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n 427|        let raw = raw_of(&half);\n 428|        let inner = Arc::new(Inner {\n 429|            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n...\n 520|            if now >= deadline {\n*521|                // The write we queued behind is stuck past OUR whole bound:\n 522|                // that op is the wedge. Abort it out of band and fail — the\n 523|                // watchdog's own deadline (armed at that op's entry) is also\n 524|                // in flight; both paths converge on the same idempotent\n## inject.rs\n 3|//! An **spt-hosted, relay-less** endpoint is one the broker holds directly (its\n*4|//! PTY is broker-owned, it is online + broker-PTY-controllable) with NO `api\n 5|//! listen` relay (no advertised TCP address). These are exactly the endpoints a\n 6|//! plain delivery would otherwise SILENTLY SPOOL — `deliver_tcp` finds no address\n 7|//! — so they must instead be reached by injecting the `<EVENT>` into the\n...\n 30|/// `api listen` relay: it is online, has no resolvable TCP relay address, and is\n*31|/// broker-PTY-`controllable`. A harness-hosted endpoint with a relay\n 32|/// (`resolve_address` is `Some`) takes the normal TCP path; an offline or\n*33|/// non-controllable perch is not a PTY-inject target. Factored verbatim from\n 34|/// `cmd_send`'s predicate so the WAN ingress shares ONE definition.\n 35|// [impl->REQ-WAN-SPT-HOSTED-DELIVERY]\n 36|// [impl->REQ-SEND-SPT-HOSTED]\n...\n 47|    ))\n*48|    .and_then(|r| r.controllable)\n 49|        == Some(true)\n 50|}\n 51|\n...\n 180|\n*181|    /// Establish an on-disk perch that reads ONLINE + broker-PTY-controllable: the\n 182|    /// class the inject leg targets.\n 183|    fn establish_hosted(id: &str) -> std::path::PathBuf {\n 184|        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n...\n 186|        let mut rec = InfoJson::new(id, \"0\", std::process::id(), \"sid\", \"live_agent\");\n*187|        rec.controllable = Some(true);\n 188|        info::write_info(&perch_path, &rec).unwrap();\n 189|        std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 190|        info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n...\n 195|    // predicate the WAN ingress and local `spt send` both gate on: an online,\n*196|    // controllable, relay-LESS endpoint is an spt-hosted inject target; a relay\n*197|    // address (harness-hosted), a non-controllable perch, or an offline one is not.\n 198|    #[test]\n 199|    fn shared_inject_leg_detects_spt_hosted() {\n 200|        crate::test_home::with_home(|_| {\n...\n 204|\n*205|            // online + controllable + no relay → inject target.\n 206|            assert!(\n 207|                is_spt_hosted_no_relay(id, &owlery),\n*208|                \"online + controllable + no relay is an spt-hosted inject target\"\n 209|            );\n 210|\n 211|            // A relay listener registered → normal TCP delivery, NOT inject.\n...\n 219|\n*220|            // Not controllable (harness-hosted live agent) → not an inject target.\n 221|            // [unit->REQ-LIVE-AGENT-NO-INJECT-DELIVERY] F-033 leg (a): a\n 222|            // harness-hosted live agent is structurally excluded from PTY-inject by\n*223|            // this predicate — bind_from_seed stamps controllable=Some(false), and\n*224|            // an unset (None) field is equally non-controllable. Both shapes below.\n 225|            let mut rec = info::read_info(&perch_path).unwrap();\n*226|            rec.controllable = None;\n 227|            info::write_info(&perch_path, &rec).unwrap();\n 228|            info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 229|            assert!(\n 230|                !is_spt_hosted_no_relay(id, &owlery),\n*231|                \"non-controllable (None) is not an inject target\"\n 232|            );\n 233|            let mut rec = info::read_info(&perch_path).unwrap();\n*234|            rec.controllable = Some(false);\n 235|            info::write_info(&perch_path, &rec).unwrap();\n 236|            info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 237|            assert!(\n 238|                !is_spt_hosted_no_relay(id, &owlery),\n*239|                \"controllable=Some(false) (bind_from_seed's harness-hosted stamp) is not an inject target\"\n 240|            );\n 241|\n 242|            // Offline → not an inject target regardless.\n 243|            let mut rec = info::read_info(&perch_path).unwrap();\n*244|            rec.controllable = Some(true);\n 245|            info::write_info(&perch_path, &rec).unwrap();\n 246|            info::set_status(&perch_path, STATUS_OFFLINE).unwrap();\n 247|            assert!(\n...\n 262|            let id = \"parked-x\";\n*263|            let perch_path = establish_hosted(id); // online + controllable + no relay\n 264|\n 265|            // No pending rows → nothing to re-offer.\n 266|            assert!(\n## lifecycle.rs\n 784|    ///\n*785|    /// Registry advertisement follows on the pump's next `advertise_local`\n*786|    /// scan — the persisted record is what it reads, epoch-bumped per row.\n 787|    // [impl->REQ-INST-3]\n 788|    // [impl->REQ-INST-4]\n 789|    pub fn rest_event(\n...\n 964|    /// endpoint readers. It is a dedicated `psyche-custody.json` (`{\"session_id\":\"…\"}`),\n*965|    /// NOT an `info.json`, so the perch scans (which key off `info.json`) never see it.\n 966|    ///\n 967|    /// Reseed (FORK-3; W3 amendment): a turn that fails with the RESERVED EXIT CODE 95\n 968|    /// (the psyche's own harness session is gone/expired) is the ONLY failure that\n## livehost.rs\n 17|//! process still first-hosts its live agents today; those perches are\n*18|//! `status`-ABSENT (not \"online\"), so this scan ignores them — no double-host in\n 19|//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the\n 20|//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this\n 21|//! reconcile reacts within one tick, the same way the shell waker reacts to a\n...\n 488|///   / already-offline endpoint is NOT resurrected).\n*489|/// - `controllable != Some(true)` → Skip (the doyle-Q1 spt-hosted discriminator, source-\n 490|///   definitive: `Some(true)` = a broker-PTY spt-hosted session ONLY; a user-terminal\n 491|///   harness-hosted agent is `Some(false)`, a legacy/adapterless record `None` — both\n 492|///   fall OUT, the safe direction, no dup risk).\n...\n 510|    status: Option<&str>,\n*511|    controllable: Option<bool>,\n 512|    in_live_session: bool,\n 513|    relay_present: bool,\n 514|    custody_pid_alive: bool,\n...\n 521|    }\n*522|    if controllable != Some(true) {\n 523|        return RestartResume::Skip;\n 524|    }\n 525|    if in_live_session {\n...\n 568|            info.status.as_deref(),\n*569|            info.controllable,\n 570|            live.contains(&id),\n 571|            relay_present,\n 572|            custody_pid_alive,\n...\n 644|/// B2 KEYSTONE — PULL liveness reconcile (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE):\n*645|/// mark OFFLINE any spt-hosted (controllable) `status=online` `live_agent` perch\n 646|/// whose broker session is GONE — the correction for the one-way `status=online`\n 647|/// latch (the broker exit-waiter reaps its session but never clears `info.json`).\n 648|/// This is the LIVE-STATUS analog of the v0.11.0 roster ghost-heal\n*649|/// (`advertise_local` → Offline): same crash-robust, self-healing-next-tick shape.\n 650|///\n 651|/// `live_sessions` is the set of endpoint ids the broker currently hosts a PTY\n 652|/// session for ([`crate::msg::SessionsReply`] via `KIND_SESSIONS`). A perch that\n*653|/// is online + controllable but ABSENT from it has a dead/exited harness → mark it\n 654|/// offline; the existing [`reconcile_once`] stop-side then un-hosts its driver and\n 655|/// the start-side stops re-hosting it.\n 656|///\n*657|/// **Controllable gate (load-bearing):** only `controllable == Some(true)`\n 658|/// (spt-hosted, broker-PTY) perches are reconciled here. A HARNESS-HOSTED relay\n*659|/// live agent (`api listen`, `controllable == Some(false)`) is legitimately online\n 660|/// with NO broker session (its liveness is the relay + ready marker), so it is\n*661|/// NEVER marked offline by this pass. A `controllable == None` perch (legacy /\n 662|/// dead-psyche like wall-a) is also exempt here — its liveness/phantom class is\n 663|/// not the broker-session arm's concern. (The former residency-confirm arm that\n 664|/// once handled it was retired with the resident-Psyche model in F-030; a\n...\n 685|        // B3 (REQ-PRESENCE-CONTROL-REAP-ON-EXIT): control stamps must not outlive the\n*686|        // broker session that owns them. A spt-hosted (controllable Some(true)) live\n 687|        // agent with NO live broker session must carry NEITHER `controlled` NOR\n 688|        // `driven_by` — reaped here REGARDLESS of status so a perch that is ALREADY\n 689|        // OFFLINE yet still reads controlled=true / driven_by=Some (the sticky stamp\n...\n 695|        // the same argument as the Gap-B driven_by self-heal). A harness-hosted relay\n*696|        // (controllable Some(false)) never carries these stamps (no broker PTY to\n*697|        // drive), so the controllable gate is exact. This is also the BOOT-TIME sweep\n 698|        // (path iv): the caller runs the reconcile from boot tick 1.\n 699|        // [impl->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n 700|        if !has_session\n*701|            && info.controllable == Some(true)\n 702|            && (info.controlled || info.driven_by.is_some())\n 703|        {\n 704|            let _ = spt_store::info::set_controlled(&perch, false);\n...\n 717|        // are exempt. An `unbound` skeleton is ALWAYS spt-hosted (bind hasn't run\n*718|        // yet, so `controllable` may be unset) — skip the gate for it.\n 719|        // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n*720|        if !is_unbound && info.controllable != Some(true) {\n 721|            continue; // harness-hosted relay / legacy — exempt\n 722|        }\n 723|        if !has_session {\n...\n 730|            // is gone, so the `clear_controller` re-stamp can never fire to unlatch\n*731|            // it. The B3 control-reap above already cleared it for a controllable\n*732|            // Some(true) perch; this belt covers an unbound skeleton (controllable\n 733|            // may be unset, so it skips the reap gate). RACE-FREE for the same\n 734|            // no-live-session reason.\n 735|            // [impl->REQ-HAZARD-DRIVEN-BY-SELFHEAL]\n...\n 1037|            // returns the REAL session set: empty at a cold start (every online\n*1038|            // controllable perch is a phantom → offlined), the surviving sessions at\n 1039|            // a brain restart (live agents stay, only the dead are offlined). The\n## notif.rs\n 696|                \"ling\",\n*697|                spt_net::net::registry::Instance {\n 698|                    node: \"remote-hex\".into(),\n 699|                    status: spt_net::net::registry::Status::Active,\n 700|                    epoch: 1,\n...\n 706|                    endpoint_type: None,\n*707|                    bound: true,\n 708|                    controller_node: None,\n*709|                    harness_only: false,\n 710|                    adapter: None,\n 711|                    recent_projects: Vec::new(),\n 712|                    controlled: false,\n## pairhost.rs\n 39|\n*40|/// The ceremony bound: aligned with the rate limiter's own stale-slot\n 41|/// reclaim so an abandoned slot and an abandoned task expire together.\n 42|const CEREMONY_TIMEOUT: Duration = Duration::from_secs(CEREMONY_TIMEOUT_SECS);\n 43|\n## presence.rs\n 6|//! - **local** — the live Self perches' `last_active_ms` stamps (the\n*7|//!   [`crate::notif::most_recently_active_where`] scan, unchanged);\n 8|//! - **subnet-wide** — the presence datum gossiped on the registry rows\n 9|//!   (D6a: `Instance.last_active_ms`, riding the epoch lease + replication,\n 10|//!   visibility-gated at advertisement), read from the daemon's per-subnet\n...\n 88|/// no identity ⇒ empty string, which matches no registry row, so every row\n*89|/// counts as remote and the local leg still resolves through the perch scan.\n 90|pub fn local_node_hex() -> String {\n 91|    if !spt_store::perch::node_key_file().exists() {\n 92|        return String::new();\n...\n 114|\n*115|    // Local leg: the live-perch scan (recency 0 when never stamped — a live\n 116|    // seat beats nothing-anywhere, the pre-D6 fallback semantics).\n 117|    if let Some(t) = most_recently_active_where(owlery, &admit_local) {\n 118|        best = Some(PresenceTarget {\n...\n 216|\n*217|    fn row(node: &str, status: Status, ts: Option<u64>) -> Instance {\n*218|        Instance {\n 219|            node: node.into(),\n 220|            status,\n 221|            epoch: 1,\n...\n 227|            endpoint_type: None,\n*228|            bound: true,\n 229|            controller_node: None,\n*230|            harness_only: false,\n 231|            adapter: None,\n 232|            recent_projects: Vec::new(),\n 233|            controlled: false,\n## projwriter.rs\n 11|//!   per cycle (`branch_tips_by_recency`, a single git spawn), at most ONE\n*12|//!   tree scan per CHANGED `p-*` branch (`ls_files_at`), ONE derivation per\n 13|//!   distinct normalized cwd (a stamped cross-cycle cache; ordinary commits\n 14|//!   in a project repo touch neither the path nor `.git/config`, so they are\n 15|//!   a NO-OP by construction — ADR-0037 §3);\n...\n 20|//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n*21|//!   scan;\n 22|//! - maintains an observability sidecar (`project-index-stats.json`) — index\n 23|//!   presence alone is not health: generated time, source generation, pending\n 24|//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n...\n 64|    pub branch_enumerations: u64,\n*65|    /// Tree scans this cycle (contract: ≤1 per CHANGED `p-*` branch).\n*66|    pub tree_scans: u64,\n 67|    /// cwd derivations this cycle (contract: ≤1 per distinct normalized cwd).\n 68|    pub derivations: u64,\n 69|}\n...\n 73|        total.branch_enumerations += self.branch_enumerations;\n*74|        total.tree_scans += self.tree_scans;\n 75|        total.derivations += self.derivations;\n 76|    }\n 77|}\n...\n 169|\n*170|/// One scanned branch: its tip at scan time + the endpoint ids holding\n 171|/// `<id>/project-context.md` in that tip tree.\n 172|#[derive(Debug, Clone)]\n 173|struct BranchScan {\n...\n 196|    pub published: bool,\n*197|    /// The cycle proved the store unchanged and exited without scanning,\n 198|    /// deriving, or writing — the warm-start / no-op contract.\n 199|    pub skipped_unchanged: bool,\n 200|}\n...\n 210|    last_fingerprint: String,\n*211|    /// branch → last scan (tips + membership), the ≤1-scan-per-changed-branch\n*212|    /// cache. Brain-local: a brain restart re-scans (bounded by B) only when\n 213|    /// the fingerprint moved or rows must be recomposed.\n 214|    branch_cache: BTreeMap<String, BranchScan>,\n 215|    /// `p-*` branches in recency order as of the last enumeration:\n...\n 269|    /// fingerprint and no scoped work SKIPS everything — the warm-start\n*270|    /// no-scan contract. Any error leaves the published index untouched\n 271|    /// (last-known-good) and surfaces in `stats.last_error`.\n 272|    // [impl->REQ-PROJECT-INDEX-WRITER]\n 273|    pub fn reconcile(&mut self, req: &Coalesced, force_full: bool) -> Result<CycleReport, String> {\n...\n 306|\n*307|        // ── 2. The no-work exit: unchanged generation performs NO scan ───\n 308|        if !fingerprint_changed\n 309|            && !force_full\n 310|            && req.endpoints.is_empty()\n...\n 318|\n*319|        // ── 3. ≤1 tree scan per changed branch ───────────────────────────\n 320|        self.branch_order = tips\n 321|            .iter()\n 322|            .filter_map(|(b, _)| b.strip_prefix(\"p-\").map(|p| (p.to_string(), b.clone())))\n...\n 330|                if cached.map(|c| &c.tip) == Some(tip) {\n*331|                    continue; // unchanged tip → cached membership, no scan\n 332|                }\n 333|                let files = store\n 334|                    .ls_files_at(tip)\n*335|                    .map_err(|e| format!(\"tree scan {branch}: {e}\"))?;\n*336|                counters.tree_scans += 1;\n 337|                let members = files\n 338|                    .iter()\n 339|                    .filter_map(|f| {\n...\n 423|                        .get(branch)\n*424|                        .is_some_and(|scan| scan.members.contains(id))\n 425|                })\n 426|                .map(|(project, _)| project.clone())\n 427|                .collect();\n...\n 614|/// construction) that loads the persisted index, runs the boot reconcile\n*615|/// (fingerprint-gated: a warm start with an unchanged generation scans\n 616|/// nothing), then drains + debounces + coalesces the invalidation queue and\n 617|/// runs the periodic full backstop until `stop`. Mirrors\n 618|/// `livehost::spawn_live_host`; the thread dies with the brain and the next\n...\n 637|        // Boot reconcile: global but NOT forced — a warm start over an\n*638|        // unchanged store exits without a scan (the cold/warm start gate).\n 639|        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n 640|        let boot = Coalesced { global: true, ..boot };\n 641|        if let Err(e) = engine.reconcile(&boot, false) {\n...\n 768|    // cycle over P=3 perches / B=2 project branches / C=2 distinct cwds does\n*769|    // EXACTLY 1 branch enumeration, 2 tree scans, 2 derivations; a second full\n*770|    // cycle over the unchanged store does 1 enumeration, 0 scans, 0 derivations\n 771|    // (memberships cached by tip, cwds cached by identity stamp); and after ONE\n## psyrelay.rs\n 4|//! The Psyche is sandboxed; the daemon is its **sole outbound proxy**\n*5|//! (KNOWN-HAZARDS 7.3). [`spt_live::outbound::parse_psyche_intents`] already\n 6|//! made a Psyche-supplied `from=`/target *unrepresentable* (the structural\n 7|//! strip); this module enforces the other half of the trust boundary at relay\n 8|//! time:\n...\n 31|\n*32|use spt_live::outbound::{parse_psyche_intents, PsycheIntent};\n 33|use spt_live::turn::run_psyche_turn;\n 34|use spt_live::TurnError;\n 35|use spt_msg::deliver::{send, SendOutcome};\n## reconcile.rs\n 48|\n*49|    fn inst(node: &str, status: Status) -> Instance {\n*50|        Instance {\n 51|            node: node.to_string(),\n 52|            status,\n 53|            epoch: 1,\n...\n 59|            endpoint_type: None,\n*60|            bound: true,\n 61|            controller_node: None,\n*62|            harness_only: false,\n 63|            adapter: None,\n 64|            recent_projects: Vec::new(),\n 65|            controlled: false,\n## resting.rs\n 224|// [impl->REQ-EFFECTIVE-INSTANCE-STATE]\n*225|pub fn effective_rest_state(alive: bool, unbound: bool, intent: Option<RestState>) -> RestState {\n 226|    if alive {\n 227|        match intent {\n 228|            Some(RestState::Active) | None => RestState::Active,\n## seedmap.rs\n 276|// [impl->REQ-HAZARD-DAEMON-STOP-BARRIER]\n*277|fn ping_bounded(name: &str, bound: std::time::Duration) -> io::Result<()> {\n 278|    let name = name.to_string();\n 279|    let (tx, rx) = std::sync::mpsc::channel();\n 280|    thread::spawn(move || {\n...\n 303|    name: &str,\n*304|    bound: std::time::Duration,\n 305|) -> io::Result<interprocess::local_socket::Stream> {\n 306|    let n = name.to_string();\n 307|    let (tx, rx) = std::sync::mpsc::channel();\n...\n 325|/// BEFORE the serve loop drops its listener, so returning on the bare ack leaves\n*326|/// a window where the socket is still bound: a following `is_running` ping (the\n 327|/// `daemon start` liveness check) wins that window and start falsely reports\n 328|/// ALREADY_RUNNING. We close it by polling [`ping`] to failure after the ack —\n 329|/// when the probe can no longer reach a listener, the stop has fully completed\n## service.rs\n 16|//! **Windows = the at-logon scheduled task is boot-only**, NOT a start/stop-\n*17|//! controllable service (DAEMON-LIFECYCLE decision b): `detected()` is false,\n 18|//! so start stays a detached spawn and stop stays an IPC stop; the task is\n 19|//! surfaced only as a `daemon status` boot hint.\n 20|\n...\n 95|pub trait DaemonService {\n*96|    /// A controllable manager is registered — a cheap probe (file/registry\n 97|    /// check, no subprocess). When true, start/stop route through it.\n 98|    fn detected(&self) -> bool;\n 99|    /// The service is currently active (the manager reports it running).\n...\n 105|    /// A boot-autostart note for `daemon status`, when the platform has one\n*106|    /// that isn't a start/stop-controllable service (the Windows logon task,\n 107|    /// or the systemd unit's enable-state). `None` = nothing to add.\n 108|    fn boot_hint(&self) -> Option<String> {\n 109|        None\n...\n 198|\n*199|// ── Windows: at-logon scheduled task (boot-only, NOT start/stop-controllable) ─\n 200|\n 201|#[cfg(windows)]\n 202|fn logon_task_present() -> bool {\n...\n 248|impl DaemonService for WindowsLogonTask {\n*249|    // The logon task is a boot trigger, not a controllable service — so\n 250|    // start/stop never route through it (decision b); `detected()` is false.\n 251|    fn detected(&self) -> bool {\n 252|        false\n...\n 258|        Err(io::Error::other(\n*259|            \"no controllable service on Windows (the logon task is boot-only)\",\n 260|        ))\n 261|    }\n 262|    fn stop(&self) -> io::Result<()> {\n 263|        Err(io::Error::other(\n*264|            \"no controllable service on Windows (the logon task is boot-only)\",\n 265|        ))\n 266|    }\n 267|    fn label(&self) -> &'static str {\n## shelldisc.rs\n 321|\n*322|        let inst = |node: &str, status: Status, shells: &[&str]| Instance {\n 323|            node: node.into(),\n 324|            status,\n 325|            epoch: 1,\n...\n 331|            endpoint_type: None,\n*332|            bound: true,\n 333|            controller_node: None,\n*334|            harness_only: false,\n 335|            adapter: None,\n 336|            recent_projects: Vec::new(),\n 337|            controlled: false,\n## shellhost.rs\n 608|        let owlery = tmp.path();\n*609|        // Owners must be bound Self perches for the bind scan to see them.\n 610|        for owner in [\"doyle\", \"ling\"] {\n 611|            let p = owlery.join(owner);\n 612|            std::fs::create_dir_all(&p).unwrap();\n\n## pump/\n### mod.rs\n 11|//!\n*12|//! - **registry push** ([`registry`], [`RegistryHost::advertise_local`] → one\n 13|//!   feed stream per peer): the node re-advertises its own rows; peers\n 14|//!   reconverge within one cadence (replay-is-recovery, the D4d/D8 push\n 15|//!   posture). Mesh-D6 widened the push target from the directly-paired set to\n...\n 219|pub struct PumpPaths {\n*220|    /// The perch tree (registry advertisement scan).\n 221|    pub owlery: PathBuf,\n 222|    /// The context store root ([`ContextStore::open_or_init_in`]).\n 223|    pub ctx_root: PathBuf,\n### registry.rs\n 63|/// The registry leg as a [`PumpWorker`]: own-row re-advertisement\n*64|/// ([`RegistryHost::advertise_local`] → one feed stream per peer). The node\n 65|/// re-advertises its own rows; peers reconverge within one cadence\n 66|/// (replay-is-recovery). Own rows only — a relayed third-party row would carry\n 67|/// an origin the receiver cannot gate honestly (KNOWN-HAZARDS 7.5/4.10).\n...\n 70|    registry: Arc<RegistryHost>,\n*71|    /// The perch tree scanned for advertisable rows.\n 72|    owlery: PathBuf,\n 73|    /// `subnet.json` — read by the due-rotation leg.\n 74|    subnets: PathBuf,\n...\n 115|    /// registry leg NOW. A wake of the pump loop, not a second advertisement\n*116|    /// path: the forced round is the same advertise_local + push code, so the\n 117|    /// epoch lease and visibility gates ride unchanged. The take IS the remove,\n 118|    /// so one wake claims exactly one forced round.\n 119|    // [impl->REQ-CONV-2]\n...\n 154|        // per-subnet `peer_step` filter below.\n*155|        self.adverts = self.registry.advertise_local(&self.owlery);\n 156|    }\n 157|\n 158|    /// Push this node's own rows for `subnet` over the peer's feed stream\n\nShowing files 1-20 of 22+. Use skip=20 for the next page, or narrow paths/pattern."}],"details":{"scopePath":"crates/spt-daemon/src/registryhost.rs, crates/spt-daemon/src/pump, crates/spt-daemon/src","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":126,"fileCount":20,"files":["crates/spt-daemon/src/registryhost.rs","crates/spt-daemon/src/pump/mod.rs","crates/spt-daemon/src/pump/registry.rs","crates/spt-daemon/src/access.rs","crates/spt-daemon/src/brain.rs","crates/spt-daemon/src/conn.rs","crates/spt-daemon/src/inject.rs","crates/spt-daemon/src/lifecycle.rs","crates/spt-daemon/src/livehost.rs","crates/spt-daemon/src/notif.rs","crates/spt-daemon/src/pairhost.rs","crates/spt-daemon/src/presence.rs","crates/spt-daemon/src/projwriter.rs","crates/spt-daemon/src/psyrelay.rs","crates/spt-daemon/src/reconcile.rs","crates/spt-daemon/src/resting.rs","crates/spt-daemon/src/seedmap.rs","crates/spt-daemon/src/service.rs","crates/spt-daemon/src/shelldisc.rs","crates/spt-daemon/src/shellhost.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/registryhost.rs","count":20},{"path":"crates/spt-daemon/src/pump/mod.rs","count":2},{"path":"crates/spt-daemon/src/pump/registry.rs","count":4},{"path":"crates/spt-daemon/src/access.rs","count":2},{"path":"crates/spt-daemon/src/brain.rs","count":1},{"path":"crates/spt-daemon/src/conn.rs","count":3},{"path":"crates/spt-daemon/src/inject.rs","count":19},{"path":"crates/spt-daemon/src/lifecycle.rs","count":3},{"path":"crates/spt-daemon/src/livehost.rs","count":20},{"path":"crates/spt-daemon/src/notif.rs","count":3},{"path":"crates/spt-daemon/src/pairhost.rs","count":1},{"path":"crates/spt-daemon/src/presence.rs","count":7},{"path":"crates/spt-daemon/src/projwriter.rs","count":20},{"path":"crates/spt-daemon/src/psyrelay.rs","count":2},{"path":"crates/spt-daemon/src/reconcile.rs","count":4},{"path":"crates/spt-daemon/src/resting.rs","count":1},{"path":"crates/spt-daemon/src/seedmap.rs","count":3},{"path":"crates/spt-daemon/src/service.rs","count":7},{"path":"crates/spt-daemon/src/shelldisc.rs","count":3},{"path":"crates/spt-daemon/src/shellhost.rs","count":1}],"truncated":true,"fileLimitReached":20,"perFileLimitReached":20,"displayContent":"# crates/spt-daemon/src/\n## registryhost.rs\n   13│//!   KNOWN-HAZARDS 7.5);\n  *14│//! - **local**: [`advertise_local`](RegistryHost::advertise_local) scans the\n   15│//!   owlery and (re-)advertises every visible self perch with a fresh\n   16│//!   [`EpochSource`] epoch — the per-node monotonic lease key, never\n   17│//!   wall-clock (KNOWN-HAZARDS 4.8).\n     │...\n  344│    // [impl->REQ-INST-7]\n *345│    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {\n  346│        let subnets = SubnetStore::load();\n  347│        let mut vis = VisibilityStore::load();\n  348│        // First-join home adoption (D9-5, REQ-INST-15): within one\n     │...\n  364│        // broadcast=subnet ride every row this node authors — the other-node\n *365│        // discovery arm. Computed once per scan (the registration set is\n  366│        // node-level).\n  367│        // [impl->REQ-EP-5]\n  368│        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())\n     │...\n  425│            // identically instead of reducing every remote row to plain green.\n *426│            //  - bound: a bound live broker session (`is_perch_alive` is bound-gated,\n  427│            //    so an UNBOUND perch reads false — exactly the fill duality the\n  428│            //    picker keys on).\n  429│            //  - controller_node: the broker-owned `driven_by` (the driving node),\n  430│            //    if any → the remote blue \"controlled\" + `controlled by <node>` pin.\n *431│            //  - harness_only: a LiveAgent with no broker PTY seat — mirrors the\n *432│            //    picker's `controllable != Some(true)` for a `live_agent` (a legacy\n  433│            //    None self-corrects on the next bind, so it advertises harness-only\n  434│            //    until then, matching the local amber).\n  435│            // [impl->REQ-SUBNET-DISPLAY-PARITY]\n     │...\n  448│            );\n *449│            let harness_only = info.as_ref().is_some_and(|i| {\n *450│                i.state == \"live_agent\" && i.controllable != Some(true)\n  451│            });\n  452│            // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS): the endpoint's\n  453│            // real harness adapter + its recent projects + the EXPLICIT any-controller\n     │...\n  466│                let Ok(ep) = epoch.next_epoch() else { continue };\n *467│                let instance = Instance {\n  468│                    node: self.node_hex.clone(),\n  469│                    status,\n  470│                    epoch: ep,\n     │...\n  492│                    controller_node: controller_node.clone(),\n *493│                    harness_only,\n  494│                    // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS).\n  495│                    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n  496│                    adapter: adapter.clone(),\n     │...\n  526│        // `live_ids`, but its last-advertised row lingers ACTIVE in the registry —\n *527│        // this scan only refreshes perches that still exist, so the stale row is\n  528│        // otherwise immortal (no re-advertise to bump it, and own rows are excluded\n  529│        // from silent-peer eviction). Advertise such a row OFFLINE with a fresh\n  530│        // epoch: the lease orders it, so it stops showing Active locally\n     │...\n  548│                let Ok(ep) = epoch.next_epoch() else { continue };\n *549│                let instance = Instance {\n  550│                    node: self.node_hex.clone(),\n  551│                    status: Status::Offline,\n  552│                    epoch: ep,\n     │...\n  562│                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\n *563│                    bound: false,\n  564│                    controller_node: None,\n *565│                    harness_only: false,\n  566│                    adapter: None,\n  567│                    recent_projects: Vec::new(),\n  568│                    controlled: false,\n     │...\n  614│    /// map). A row whose author has NO heard stamp is treated as silent —\n *615│    /// rows only enter via admitted feeds (which stamp) or `advertise_local`\n  616│    /// (own rows, excluded), so an unstamped foreign row is already an\n  617│    /// anomaly and fails toward eviction. A merely-offline peer reconverges\n  618│    /// within one pump cadence of its return (re-advertisement from its\n     │...\n  797│/// The [`Status`] this node advertises for one of its OWN perches (the\n *798│/// self-gossip path of [`RegistryHost::advertise_local`]).\n  799│///\n  800│/// - **Bound + alive** (`is_perch_alive`): follow the resting-state machine —\n  801│///   Active / Dormant / Suspended per the perch's resting record (recordless ⇒\n     │...\n 1012│/// start/stop, a rest edge) drops it; the pump consumes it and forces a\n*1013│/// registry round NOW (advertise_local + peer push) instead of waiting out\n 1014│/// the cadence. A WAKE of the existing loop, never a second advertisement\n 1015│/// path: the forced round takes the exact same code path, so the epoch lease\n 1016│/// and visibility gates ride unchanged. File-based because the transitions\n     │...\n 1141│            endpoint_id: id.to_string(),\n*1142│            instance: Instance {\n 1143│                node: node.to_string(),\n 1144│                status: Status::Active,\n 1145│                epoch,\n     │...\n 1151│                endpoint_type: None,\n*1152│                bound: true,\n 1153│                controller_node: None,\n*1154│                harness_only: false,\n 1155│                adapter: None,\n 1156│                recent_projects: Vec::new(),\n 1157│                controlled: false,\n## access.rs\n 117│pub fn classify_inbound(endpoint: &str, origin_node: &str, now_ms: u64) -> InboundClass {\n*118│    if RecentOutbound::load().correlates(endpoint, origin_node, now_ms, REPLY_WINDOW_MS) {\n 119│        InboundClass::Reply\n 120│    } else {\n 121│        InboundClass::Unsolicited\n    │...\n 131│pub fn note_outbound(endpoint: &str, node: &str, now_ms: u64) {\n*132│    let mut log = RecentOutbound::load();\n 133│    log.record(endpoint, node, now_ms, REPLY_WINDOW_MS);\n 134│    let _ = log.save();\n 135│}\n## brain.rs\n 1691│                Some(dl) => {\n*1692│                    // Total-wait bound: each recv is the REMAINING budget to the\n 1693│                    // call's entry deadline, so unrelated frames dripping in\n 1694│                    // cannot reset it (the per-read-timeout wedge the ruling\n 1695│                    // forbids). A non-positive remaining → recv_timeout(ZERO),\n## conn.rs\n 170│    /// The absolute per-write bound (gate-wait + OS write completion).\n*171│    bound: Duration,\n 172│    /// The raw OS handle/fd of the underlying stream, captured at\n 173│    /// construction for the out-of-band abort. The recv and send halves share\n 174│    /// ONE underlying stream object (`interprocess` splits by ref-clone), so\n    │...\n 425│    /// `brain_write_deadline()` (`SPT_BRAIN_WRITE_DEADLINE_MS` knob).\n*426│    pub(crate) fn new(half: SendHalf, bound: Duration) -> Self {\n 427│        let raw = raw_of(&half);\n 428│        let inner = Arc::new(Inner {\n 429│            id: CONN_ID_SEQ.fetch_add(1, Ordering::Relaxed) + 1,\n    │...\n 520│            if now >= deadline {\n*521│                // The write we queued behind is stuck past OUR whole bound:\n 522│                // that op is the wedge. Abort it out of band and fail — the\n 523│                // watchdog's own deadline (armed at that op's entry) is also\n 524│                // in flight; both paths converge on the same idempotent\n## inject.rs\n   3│//! An **spt-hosted, relay-less** endpoint is one the broker holds directly (its\n  *4│//! PTY is broker-owned, it is online + broker-PTY-controllable) with NO `api\n   5│//! listen` relay (no advertised TCP address). These are exactly the endpoints a\n   6│//! plain delivery would otherwise SILENTLY SPOOL — `deliver_tcp` finds no address\n   7│//! — so they must instead be reached by injecting the `<EVENT>` into the\n    │...\n  30│/// `api listen` relay: it is online, has no resolvable TCP relay address, and is\n *31│/// broker-PTY-`controllable`. A harness-hosted endpoint with a relay\n  32│/// (`resolve_address` is `Some`) takes the normal TCP path; an offline or\n *33│/// non-controllable perch is not a PTY-inject target. Factored verbatim from\n  34│/// `cmd_send`'s predicate so the WAN ingress shares ONE definition.\n  35│// [impl->REQ-WAN-SPT-HOSTED-DELIVERY]\n  36│// [impl->REQ-SEND-SPT-HOSTED]\n    │...\n  47│    ))\n *48│    .and_then(|r| r.controllable)\n  49│        == Some(true)\n  50│}\n  51│\n    │...\n 180│\n*181│    /// Establish an on-disk perch that reads ONLINE + broker-PTY-controllable: the\n 182│    /// class the inject leg targets.\n 183│    fn establish_hosted(id: &str) -> std::path::PathBuf {\n 184│        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n    │...\n 186│        let mut rec = InfoJson::new(id, \"0\", std::process::id(), \"sid\", \"live_agent\");\n*187│        rec.controllable = Some(true);\n 188│        info::write_info(&perch_path, &rec).unwrap();\n 189│        std::fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\").unwrap();\n 190│        info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n    │...\n 195│    // predicate the WAN ingress and local `spt send` both gate on: an online,\n*196│    // controllable, relay-LESS endpoint is an spt-hosted inject target; a relay\n*197│    // address (harness-hosted), a non-controllable perch, or an offline one is not.\n 198│    #[test]\n 199│    fn shared_inject_leg_detects_spt_hosted() {\n 200│        crate::test_home::with_home(|_| {\n    │...\n 204│\n*205│            // online + controllable + no relay → inject target.\n 206│            assert!(\n 207│                is_spt_hosted_no_relay(id, &owlery),\n*208│                \"online + controllable + no relay is an spt-hosted inject target\"\n 209│            );\n 210│\n 211│            // A relay listener registered → normal TCP delivery, NOT inject.\n    │...\n 219│\n*220│            // Not controllable (harness-hosted live agent) → not an inject target.\n 221│            // [unit->REQ-LIVE-AGENT-NO-INJECT-DELIVERY] F-033 leg (a): a\n 222│            // harness-hosted live agent is structurally excluded from PTY-inject by\n*223│            // this predicate — bind_from_seed stamps controllable=Some(false), and\n*224│            // an unset (None) field is equally non-controllable. Both shapes below.\n 225│            let mut rec = info::read_info(&perch_path).unwrap();\n*226│            rec.controllable = None;\n 227│            info::write_info(&perch_path, &rec).unwrap();\n 228│            info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 229│            assert!(\n 230│                !is_spt_hosted_no_relay(id, &owlery),\n*231│                \"non-controllable (None) is not an inject target\"\n 232│            );\n 233│            let mut rec = info::read_info(&perch_path).unwrap();\n*234│            rec.controllable = Some(false);\n 235│            info::write_info(&perch_path, &rec).unwrap();\n 236│            info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 237│            assert!(\n 238│                !is_spt_hosted_no_relay(id, &owlery),\n*239│                \"controllable=Some(false) (bind_from_seed's harness-hosted stamp) is not an inject target\"\n 240│            );\n 241│\n 242│            // Offline → not an inject target regardless.\n 243│            let mut rec = info::read_info(&perch_path).unwrap();\n*244│            rec.controllable = Some(true);\n 245│            info::write_info(&perch_path, &rec).unwrap();\n 246│            info::set_status(&perch_path, STATUS_OFFLINE).unwrap();\n 247│            assert!(\n    │...\n 262│            let id = \"parked-x\";\n*263│            let perch_path = establish_hosted(id); // online + controllable + no relay\n 264│\n 265│            // No pending rows → nothing to re-offer.\n 266│            assert!(\n## lifecycle.rs\n 784│    ///\n*785│    /// Registry advertisement follows on the pump's next `advertise_local`\n*786│    /// scan — the persisted record is what it reads, epoch-bumped per row.\n 787│    // [impl->REQ-INST-3]\n 788│    // [impl->REQ-INST-4]\n 789│    pub fn rest_event(\n    │...\n 964│    /// endpoint readers. It is a dedicated `psyche-custody.json` (`{\"session_id\":\"…\"}`),\n*965│    /// NOT an `info.json`, so the perch scans (which key off `info.json`) never see it.\n 966│    ///\n 967│    /// Reseed (FORK-3; W3 amendment): a turn that fails with the RESERVED EXIT CODE 95\n 968│    /// (the psyche's own harness session is gone/expired) is the ONLY failure that\n## livehost.rs\n   17│//! process still first-hosts its live agents today; those perches are\n  *18│//! `status`-ABSENT (not \"online\"), so this scan ignores them — no double-host in\n   19│//! the W0.1→W0.2 window. W0.2 retires the interim and makes the brain the\n   20│//! first-host (it sets `status=online` on a live `listen`/`bind`); from then this\n   21│//! reconcile reacts within one tick, the same way the shell waker reacts to a\n     │...\n  488│///   / already-offline endpoint is NOT resurrected).\n *489│/// - `controllable != Some(true)` → Skip (the doyle-Q1 spt-hosted discriminator, source-\n  490│///   definitive: `Some(true)` = a broker-PTY spt-hosted session ONLY; a user-terminal\n  491│///   harness-hosted agent is `Some(false)`, a legacy/adapterless record `None` — both\n  492│///   fall OUT, the safe direction, no dup risk).\n     │...\n  510│    status: Option<&str>,\n *511│    controllable: Option<bool>,\n  512│    in_live_session: bool,\n  513│    relay_present: bool,\n  514│    custody_pid_alive: bool,\n     │...\n  521│    }\n *522│    if controllable != Some(true) {\n  523│        return RestartResume::Skip;\n  524│    }\n  525│    if in_live_session {\n     │...\n  568│            info.status.as_deref(),\n *569│            info.controllable,\n  570│            live.contains(&id),\n  571│            relay_present,\n  572│            custody_pid_alive,\n     │...\n  644│/// B2 KEYSTONE — PULL liveness reconcile (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE):\n *645│/// mark OFFLINE any spt-hosted (controllable) `status=online` `live_agent` perch\n  646│/// whose broker session is GONE — the correction for the one-way `status=online`\n  647│/// latch (the broker exit-waiter reaps its session but never clears `info.json`).\n  648│/// This is the LIVE-STATUS analog of the v0.11.0 roster ghost-heal\n *649│/// (`advertise_local` → Offline): same crash-robust, self-healing-next-tick shape.\n  650│///\n  651│/// `live_sessions` is the set of endpoint ids the broker currently hosts a PTY\n  652│/// session for ([`crate::msg::SessionsReply`] via `KIND_SESSIONS`). A perch that\n *653│/// is online + controllable but ABSENT from it has a dead/exited harness → mark it\n  654│/// offline; the existing [`reconcile_once`] stop-side then un-hosts its driver and\n  655│/// the start-side stops re-hosting it.\n  656│///\n *657│/// **Controllable gate (load-bearing):** only `controllable == Some(true)`\n  658│/// (spt-hosted, broker-PTY) perches are reconciled here. A HARNESS-HOSTED relay\n *659│/// live agent (`api listen`, `controllable == Some(false)`) is legitimately online\n  660│/// with NO broker session (its liveness is the relay + ready marker), so it is\n *661│/// NEVER marked offline by this pass. A `controllable == None` perch (legacy /\n  662│/// dead-psyche like wall-a) is also exempt here — its liveness/phantom class is\n  663│/// not the broker-session arm's concern. (The former residency-confirm arm that\n  664│/// once handled it was retired with the resident-Psyche model in F-030; a\n     │...\n  685│        // B3 (REQ-PRESENCE-CONTROL-REAP-ON-EXIT): control stamps must not outlive the\n *686│        // broker session that owns them. A spt-hosted (controllable Some(true)) live\n  687│        // agent with NO live broker session must carry NEITHER `controlled` NOR\n  688│        // `driven_by` — reaped here REGARDLESS of status so a perch that is ALREADY\n  689│        // OFFLINE yet still reads controlled=true / driven_by=Some (the sticky stamp\n     │...\n  695│        // the same argument as the Gap-B driven_by self-heal). A harness-hosted relay\n *696│        // (controllable Some(false)) never carries these stamps (no broker PTY to\n *697│        // drive), so the controllable gate is exact. This is also the BOOT-TIME sweep\n  698│        // (path iv): the caller runs the reconcile from boot tick 1.\n  699│        // [impl->REQ-PRESENCE-CONTROL-REAP-ON-EXIT]\n  700│        if !has_session\n *701│            && info.controllable == Some(true)\n  702│            && (info.controlled || info.driven_by.is_some())\n  703│        {\n  704│            let _ = spt_store::info::set_controlled(&perch, false);\n     │...\n  717│        // are exempt. An `unbound` skeleton is ALWAYS spt-hosted (bind hasn't run\n *718│        // yet, so `controllable` may be unset) — skip the gate for it.\n  719│        // [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\n *720│        if !is_unbound && info.controllable != Some(true) {\n  721│            continue; // harness-hosted relay / legacy — exempt\n  722│        }\n  723│        if !has_session {\n     │...\n  730│            // is gone, so the `clear_controller` re-stamp can never fire to unlatch\n *731│            // it. The B3 control-reap above already cleared it for a controllable\n *732│            // Some(true) perch; this belt covers an unbound skeleton (controllable\n  733│            // may be unset, so it skips the reap gate). RACE-FREE for the same\n  734│            // no-live-session reason.\n  735│            // [impl->REQ-HAZARD-DRIVEN-BY-SELFHEAL]\n     │...\n 1037│            // returns the REAL session set: empty at a cold start (every online\n*1038│            // controllable perch is a phantom → offlined), the surviving sessions at\n 1039│            // a brain restart (live agents stay, only the dead are offlined). The\n## notif.rs\n 696│                \"ling\",\n*697│                spt_net::net::registry::Instance {\n 698│                    node: \"remote-hex\".into(),\n 699│                    status: spt_net::net::registry::Status::Active,\n 700│                    epoch: 1,\n    │...\n 706│                    endpoint_type: None,\n*707│                    bound: true,\n 708│                    controller_node: None,\n*709│                    harness_only: false,\n 710│                    adapter: None,\n 711│                    recent_projects: Vec::new(),\n 712│                    controlled: false,\n## pairhost.rs\n 39│\n*40│/// The ceremony bound: aligned with the rate limiter's own stale-slot\n 41│/// reclaim so an abandoned slot and an abandoned task expire together.\n 42│const CEREMONY_TIMEOUT: Duration = Duration::from_secs(CEREMONY_TIMEOUT_SECS);\n 43│\n## presence.rs\n   6│//! - **local** — the live Self perches' `last_active_ms` stamps (the\n  *7│//!   [`crate::notif::most_recently_active_where`] scan, unchanged);\n   8│//! - **subnet-wide** — the presence datum gossiped on the registry rows\n   9│//!   (D6a: `Instance.last_active_ms`, riding the epoch lease + replication,\n  10│//!   visibility-gated at advertisement), read from the daemon's per-subnet\n    │...\n  88│/// no identity ⇒ empty string, which matches no registry row, so every row\n *89│/// counts as remote and the local leg still resolves through the perch scan.\n  90│pub fn local_node_hex() -> String {\n  91│    if !spt_store::perch::node_key_file().exists() {\n  92│        return String::new();\n    │...\n 114│\n*115│    // Local leg: the live-perch scan (recency 0 when never stamped — a live\n 116│    // seat beats nothing-anywhere, the pre-D6 fallback semantics).\n 117│    if let Some(t) = most_recently_active_where(owlery, &admit_local) {\n 118│        best = Some(PresenceTarget {\n    │...\n 216│\n*217│    fn row(node: &str, status: Status, ts: Option<u64>) -> Instance {\n*218│        Instance {\n 219│            node: node.into(),\n 220│            status,\n 221│            epoch: 1,\n    │...\n 227│            endpoint_type: None,\n*228│            bound: true,\n 229│            controller_node: None,\n*230│            harness_only: false,\n 231│            adapter: None,\n 232│            recent_projects: Vec::new(),\n 233│            controlled: false,\n## projwriter.rs\n  11│//!   per cycle (`branch_tips_by_recency`, a single git spawn), at most ONE\n *12│//!   tree scan per CHANGED `p-*` branch (`ls_files_at`), ONE derivation per\n  13│//!   distinct normalized cwd (a stamped cross-cycle cache; ordinary commits\n  14│//!   in a project repo touch neither the path nor `.git/config`, so they are\n  15│//!   a NO-OP by construction — ADR-0037 §3);\n    │...\n  20│//!   (`projindex::fingerprint_tips`): an unchanged generation performs NO\n *21│//!   scan;\n  22│//! - maintains an observability sidecar (`project-index-stats.json`) — index\n  23│//!   presence alone is not health: generated time, source generation, pending\n  24│//!   refresh, last duration/error, counts, cache hits/misses, stale reads,\n    │...\n  64│    pub branch_enumerations: u64,\n *65│    /// Tree scans this cycle (contract: ≤1 per CHANGED `p-*` branch).\n *66│    pub tree_scans: u64,\n  67│    /// cwd derivations this cycle (contract: ≤1 per distinct normalized cwd).\n  68│    pub derivations: u64,\n  69│}\n    │...\n  73│        total.branch_enumerations += self.branch_enumerations;\n *74│        total.tree_scans += self.tree_scans;\n  75│        total.derivations += self.derivations;\n  76│    }\n  77│}\n    │...\n 169│\n*170│/// One scanned branch: its tip at scan time + the endpoint ids holding\n 171│/// `<id>/project-context.md` in that tip tree.\n 172│#[derive(Debug, Clone)]\n 173│struct BranchScan {\n    │...\n 196│    pub published: bool,\n*197│    /// The cycle proved the store unchanged and exited without scanning,\n 198│    /// deriving, or writing — the warm-start / no-op contract.\n 199│    pub skipped_unchanged: bool,\n 200│}\n    │...\n 210│    last_fingerprint: String,\n*211│    /// branch → last scan (tips + membership), the ≤1-scan-per-changed-branch\n*212│    /// cache. Brain-local: a brain restart re-scans (bounded by B) only when\n 213│    /// the fingerprint moved or rows must be recomposed.\n 214│    branch_cache: BTreeMap<String, BranchScan>,\n 215│    /// `p-*` branches in recency order as of the last enumeration:\n    │...\n 269│    /// fingerprint and no scoped work SKIPS everything — the warm-start\n*270│    /// no-scan contract. Any error leaves the published index untouched\n 271│    /// (last-known-good) and surfaces in `stats.last_error`.\n 272│    // [impl->REQ-PROJECT-INDEX-WRITER]\n 273│    pub fn reconcile(&mut self, req: &Coalesced, force_full: bool) -> Result<CycleReport, String> {\n    │...\n 306│\n*307│        // ── 2. The no-work exit: unchanged generation performs NO scan ───\n 308│        if !fingerprint_changed\n 309│            && !force_full\n 310│            && req.endpoints.is_empty()\n    │...\n 318│\n*319│        // ── 3. ≤1 tree scan per changed branch ───────────────────────────\n 320│        self.branch_order = tips\n 321│            .iter()\n 322│            .filter_map(|(b, _)| b.strip_prefix(\"p-\").map(|p| (p.to_string(), b.clone())))\n    │...\n 330│                if cached.map(|c| &c.tip) == Some(tip) {\n*331│                    continue; // unchanged tip → cached membership, no scan\n 332│                }\n 333│                let files = store\n 334│                    .ls_files_at(tip)\n*335│                    .map_err(|e| format!(\"tree scan {branch}: {e}\"))?;\n*336│                counters.tree_scans += 1;\n 337│                let members = files\n 338│                    .iter()\n 339│                    .filter_map(|f| {\n    │...\n 423│                        .get(branch)\n*424│                        .is_some_and(|scan| scan.members.contains(id))\n 425│                })\n 426│                .map(|(project, _)| project.clone())\n 427│                .collect();\n    │...\n 614│/// construction) that loads the persisted index, runs the boot reconcile\n*615│/// (fingerprint-gated: a warm start with an unchanged generation scans\n 616│/// nothing), then drains + debounces + coalesces the invalidation queue and\n 617│/// runs the periodic full backstop until `stop`. Mirrors\n 618│/// `livehost::spawn_live_host`; the thread dies with the brain and the next\n    │...\n 637│        // Boot reconcile: global but NOT forced — a warm start over an\n*638│        // unchanged store exits without a scan (the cold/warm start gate).\n 639│        let boot = projinval::coalesce(projinval::drain_at(&engine.paths.invalidations_dir));\n 640│        let boot = Coalesced { global: true, ..boot };\n 641│        if let Err(e) = engine.reconcile(&boot, false) {\n    │...\n 768│    // cycle over P=3 perches / B=2 project branches / C=2 distinct cwds does\n*769│    // EXACTLY 1 branch enumeration, 2 tree scans, 2 derivations; a second full\n*770│    // cycle over the unchanged store does 1 enumeration, 0 scans, 0 derivations\n 771│    // (memberships cached by tip, cwds cached by identity stamp); and after ONE\n## psyrelay.rs\n  4│//! The Psyche is sandboxed; the daemon is its **sole outbound proxy**\n *5│//! (KNOWN-HAZARDS 7.3). [`spt_live::outbound::parse_psyche_intents`] already\n  6│//! made a Psyche-supplied `from=`/target *unrepresentable* (the structural\n  7│//! strip); this module enforces the other half of the trust boundary at relay\n  8│//! time:\n   │...\n 31│\n*32│use spt_live::outbound::{parse_psyche_intents, PsycheIntent};\n 33│use spt_live::turn::run_psyche_turn;\n 34│use spt_live::TurnError;\n 35│use spt_msg::deliver::{send, SendOutcome};\n## reconcile.rs\n 48│\n*49│    fn inst(node: &str, status: Status) -> Instance {\n*50│        Instance {\n 51│            node: node.to_string(),\n 52│            status,\n 53│            epoch: 1,\n   │...\n 59│            endpoint_type: None,\n*60│            bound: true,\n 61│            controller_node: None,\n*62│            harness_only: false,\n 63│            adapter: None,\n 64│            recent_projects: Vec::new(),\n 65│            controlled: false,\n## resting.rs\n 224│// [impl->REQ-EFFECTIVE-INSTANCE-STATE]\n*225│pub fn effective_rest_state(alive: bool, unbound: bool, intent: Option<RestState>) -> RestState {\n 226│    if alive {\n 227│        match intent {\n 228│            Some(RestState::Active) | None => RestState::Active,\n## seedmap.rs\n 276│// [impl->REQ-HAZARD-DAEMON-STOP-BARRIER]\n*277│fn ping_bounded(name: &str, bound: std::time::Duration) -> io::Result<()> {\n 278│    let name = name.to_string();\n 279│    let (tx, rx) = std::sync::mpsc::channel();\n 280│    thread::spawn(move || {\n    │...\n 303│    name: &str,\n*304│    bound: std::time::Duration,\n 305│) -> io::Result<interprocess::local_socket::Stream> {\n 306│    let n = name.to_string();\n 307│    let (tx, rx) = std::sync::mpsc::channel();\n    │...\n 325│/// BEFORE the serve loop drops its listener, so returning on the bare ack leaves\n*326│/// a window where the socket is still bound: a following `is_running` ping (the\n 327│/// `daemon start` liveness check) wins that window and start falsely reports\n 328│/// ALREADY_RUNNING. We close it by polling [`ping`] to failure after the ack —\n 329│/// when the probe can no longer reach a listener, the stop has fully completed\n## service.rs\n  16│//! **Windows = the at-logon scheduled task is boot-only**, NOT a start/stop-\n *17│//! controllable service (DAEMON-LIFECYCLE decision b): `detected()` is false,\n  18│//! so start stays a detached spawn and stop stays an IPC stop; the task is\n  19│//! surfaced only as a `daemon status` boot hint.\n  20│\n    │...\n  95│pub trait DaemonService {\n *96│    /// A controllable manager is registered — a cheap probe (file/registry\n  97│    /// check, no subprocess). When true, start/stop route through it.\n  98│    fn detected(&self) -> bool;\n  99│    /// The service is currently active (the manager reports it running).\n    │...\n 105│    /// A boot-autostart note for `daemon status`, when the platform has one\n*106│    /// that isn't a start/stop-controllable service (the Windows logon task,\n 107│    /// or the systemd unit's enable-state). `None` = nothing to add.\n 108│    fn boot_hint(&self) -> Option<String> {\n 109│        None\n    │...\n 198│\n*199│// ── Windows: at-logon scheduled task (boot-only, NOT start/stop-controllable) ─\n 200│\n 201│#[cfg(windows)]\n 202│fn logon_task_present() -> bool {\n    │...\n 248│impl DaemonService for WindowsLogonTask {\n*249│    // The logon task is a boot trigger, not a controllable service — so\n 250│    // start/stop never route through it (decision b); `detected()` is false.\n 251│    fn detected(&self) -> bool {\n 252│        false\n    │...\n 258│        Err(io::Error::other(\n*259│            \"no controllable service on Windows (the logon task is boot-only)\",\n 260│        ))\n 261│    }\n 262│    fn stop(&self) -> io::Result<()> {\n 263│        Err(io::Error::other(\n*264│            \"no controllable service on Windows (the logon task is boot-only)\",\n 265│        ))\n 266│    }\n 267│    fn label(&self) -> &'static str {\n## shelldisc.rs\n 321│\n*322│        let inst = |node: &str, status: Status, shells: &[&str]| Instance {\n 323│            node: node.into(),\n 324│            status,\n 325│            epoch: 1,\n    │...\n 331│            endpoint_type: None,\n*332│            bound: true,\n 333│            controller_node: None,\n*334│            harness_only: false,\n 335│            adapter: None,\n 336│            recent_projects: Vec::new(),\n 337│            controlled: false,\n## shellhost.rs\n 608│        let owlery = tmp.path();\n*609│        // Owners must be bound Self perches for the bind scan to see them.\n 610│        for owner in [\"doyle\", \"ling\"] {\n 611│            let p = owlery.join(owner);\n 612│            std::fs::create_dir_all(&p).unwrap();\n\n## pump/\n### mod.rs\n  11│//!\n *12│//! - **registry push** ([`registry`], [`RegistryHost::advertise_local`] → one\n  13│//!   feed stream per peer): the node re-advertises its own rows; peers\n  14│//!   reconverge within one cadence (replay-is-recovery, the D4d/D8 push\n  15│//!   posture). Mesh-D6 widened the push target from the directly-paired set to\n    │...\n 219│pub struct PumpPaths {\n*220│    /// The perch tree (registry advertisement scan).\n 221│    pub owlery: PathBuf,\n 222│    /// The context store root ([`ContextStore::open_or_init_in`]).\n 223│    pub ctx_root: PathBuf,\n### registry.rs\n  63│/// The registry leg as a [`PumpWorker`]: own-row re-advertisement\n *64│/// ([`RegistryHost::advertise_local`] → one feed stream per peer). The node\n  65│/// re-advertises its own rows; peers reconverge within one cadence\n  66│/// (replay-is-recovery). Own rows only — a relayed third-party row would carry\n  67│/// an origin the receiver cannot gate honestly (KNOWN-HAZARDS 7.5/4.10).\n    │...\n  70│    registry: Arc<RegistryHost>,\n *71│    /// The perch tree scanned for advertisable rows.\n  72│    owlery: PathBuf,\n  73│    /// `subnet.json` — read by the due-rotation leg.\n  74│    subnets: PathBuf,\n    │...\n 115│    /// registry leg NOW. A wake of the pump loop, not a second advertisement\n*116│    /// path: the forced round is the same advertise_local + push code, so the\n 117│    /// epoch lease and visibility gates ride unchanged. The take IS the remove,\n 118│    /// so one wake claims exactly one forced round.\n 119│    // [impl->REQ-CONV-2]\n    │...\n 154│        // per-subnet `peer_step` filter below.\n*155│        self.adverts = self.registry.advertise_local(&self.owlery);\n 156│    }\n 157│\n 158│    /// Push this node's own rows for `subnet` over the peer's feed stream"},"isError":false,"timestamp":1784197467236}}
{"type":"message","id":"c7598dc6","parentId":"a6170328","timestamp":"2026-07-16T10:24:27.248Z","message":{"role":"toolResult","toolCallId":"call_6eydQPhMa6D0dZEm9o327eqk|fc_017bff223290b7ba016a58b15b17e8819aafdf06564dc1ff60","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/src/\n### cli.rs\n 2136|            \"ENDPOINT_RUN_SESSION_TIMEOUT:{id}: no attachable broker session within {SESSION_AWAIT_SECS}s \\\n*2137|             — not attaching (check `spt endpoint list`; retry with `spt rc {id}`)\"\n 2138|        );\n 2139|        return 1;\n 2140|    }\n### rc.rs\n 28|\n*29|use spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\n 30|use spt_daemon::effect::{Minter, MintedOp};\n 31|use spt_daemon::brain::{now_ms, Brain, BrokerEvent};\n 32|use spt_daemon::msg::decode_bytes;\n...\n 1121|    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n*1122|    // `status=offline` is not attachable — short-circuit with clean guidance\n 1123|    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n 1124|    // instead of the broker dance that would block on a dead session (the\n 1125|    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n...\n 1429|        }\n*1430|        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n*1431|            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n 1432|                remote_node = Some(node);\n 1433|                // session_id is a placeholder the owning node ignores — it\n 1434|                // resolves the endpoint to its OWN session id server-side.\n...\n 1436|            }\n*1437|            crate::wansend::OwnerDial::NotFound => {\n 1438|                return Err(EstablishFail::NoTarget(format!(\n 1439|                    \"No live session for '{endpoint_id}' — not running on this node \\\n 1440|                     or any visible subnet.\"\n...\n 1442|            }\n*1443|            crate::wansend::OwnerDial::Ambiguous(msg) => {\n 1444|                return Err(EstablishFail::NoTarget(msg))\n 1445|            }\n*1446|            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n 1447|                return Err(EstablishFail::NoTarget(format!(\n 1448|                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n 1449|                )))\n...\n 1475|                    // Remote: re-resolve + re-dial the owning node.\n*1476|                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n*1477|                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\n 1478|                        _ => {\n 1479|                            return Err(std::io::Error::other(format!(\n 1480|                                \"'{endpoint_id}' is no longer reachable\"\n...\n 1485|            };\n*1486|            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n 1487|        },\n 1488|    )\n 1489|    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n### wansend.rs\n 85|#[derive(Debug)]\n*86|pub enum OwnerDial {\n 87|    /// The owning node was resolved and dialed; `conn_id` is the live QUIC conn,\n 88|    /// `node` its display name (label or key prefix) for any status copy.\n 89|    Dialed { conn_id: u64, node: String },\n...\n 115|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*116|pub fn resolve_and_dial_owner(brain: &mut Brain, endpoint: &str) -> OwnerDial {\n 117|    let own_hex = match spt_store::nodeid::load_or_create() {\n 118|        Ok(id) => id.public_key().to_hex(),\n 119|        Err(e) => {\n*120|            return OwnerDial::Unreachable {\n 121|                node: \"?\".to_string(),\n 122|                detail: format!(\"node identity: {e}\"),\n 123|            }\n...\n 128|        Err(e) => {\n*129|            return OwnerDial::Unreachable {\n 130|                node: \"?\".to_string(),\n 131|                detail: format!(\"bad address: {e:?}\"),\n 132|            }\n...\n 147|        Resolution::Resolved { instance, .. } => instance,\n*148|        Resolution::Ambiguous(a) => return OwnerDial::Ambiguous(render_refusal(&address.id, &a, &regs)),\n*149|        Resolution::NotFound => return OwnerDial::NotFound,\n 150|    };\n 151|    if instance.node == own_hex {\n 152|        // The registry says WE own it, but the caller already missed it in the\n 153|        // local session scan — a local staleness with no remote to dial.\n*154|        return OwnerDial::NotFound;\n 155|    }\n 156|    let node = node_display(&instance.node, &regs);\n 157|    let Some(addr) = seed_first_addr(&instance.node) else {\n*158|        return OwnerDial::Unreachable {\n 159|            node,\n 160|            detail: \"no dialable address\".to_string(),\n 161|        };\n...\n 163|    match brain.net_dial(addr, None) {\n*164|        Ok(c) => OwnerDial::Dialed {\n 165|            conn_id: c.conn_id,\n 166|            node,\n 167|        },\n*168|        Err(e) => OwnerDial::Unreachable {\n 169|            node,\n 170|            detail: e.to_string(),\n 171|        },\n...\n 1334|    /// Write a registry snapshot to the canonical on-disk path\n*1335|    /// [`resolve_and_dial_owner`] reads via [`load_snapshots`] — the production\n 1336|    /// disk path, not an injected map.\n 1337|    fn write_snapshot(subnet: &str, id: &str, node: &str) {\n 1338|        let dir = perch::identity_dir().join(\"registry\");\n...\n 1354|    #[test]\n*1355|    fn resolve_and_dial_owner_is_honest_on_resolution_misses() {\n 1356|        let _home = isolated_home();\n 1357|        let dir = tempfile::tempdir().unwrap();\n 1358|        let name = format!(\"spt-rcresolve-{}.sock\", std::process::id());\n...\n 1363|        assert!(matches!(\n*1364|            resolve_and_dial_owner(&mut a, \"ghost\"),\n*1365|            OwnerDial::NotFound\n 1366|        ));\n 1367|\n 1368|        // Our own node hosts it (registry staleness) → NotFound, no self-dial.\n...\n 1374|        assert!(matches!(\n*1375|            resolve_and_dial_owner(&mut a, \"mine\"),\n*1376|            OwnerDial::NotFound\n 1377|        ));\n 1378|\n 1379|        // Two subnets hold it → Ambiguous (refuse-and-qualify), never a guess.\n...\n 1382|        assert!(matches!(\n*1383|            resolve_and_dial_owner(&mut a, \"twin\"),\n*1384|            OwnerDial::Ambiguous(_)\n 1385|        ));\n 1386|    }\n 1387|\n...\n 1396|    #[test]\n*1397|    fn resolve_and_dial_owner_dials_the_resolved_node() {\n 1398|        use spt_store::peeraddrs::{peer_addrs_file, PeerAddrStore};\n 1399|        let _home = isolated_home();\n 1400|        let dir = tempfile::tempdir().unwrap();\n...\n 1420|\n*1421|        match resolve_and_dial_owner(&mut a, \"ling-remote\") {\n\n## spt-daemon/src/\n### attach.rs\n 560|) -> io::Result<u64> {\n*561|    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\n 562|}\n 563|\n 564|/// Like [`request_attach`], but for the cross-node leg (#4,\n...\n 571|// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*572|pub fn request_attach_endpoint(\n 573|    brain: &mut Brain,\n 574|    conn_id: u64,\n 575|    session_id: u64,"}],"details":{"scopePath":"crates/spt/src/cli.rs, crates/spt/src/rc.rs, crates/spt-daemon/src/attach.rs, crates/spt/src/wansend.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":33,"fileCount":4,"files":["crates/spt/src/cli.rs","crates/spt/src/rc.rs","crates/spt-daemon/src/attach.rs","crates/spt/src/wansend.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":1},{"path":"crates/spt/src/rc.rs","count":10},{"path":"crates/spt-daemon/src/attach.rs","count":2},{"path":"crates/spt/src/wansend.rs","count":20}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/\n\n## spt/src/\n### cli.rs\n 2136│            \"ENDPOINT_RUN_SESSION_TIMEOUT:{id}: no attachable broker session within {SESSION_AWAIT_SECS}s \\\n*2137│             — not attaching (check `spt endpoint list`; retry with `spt rc {id}`)\"\n 2138│        );\n 2139│        return 1;\n 2140│    }\n### rc.rs\n   28│\n  *29│use spt_daemon::attach::{request_attach_endpoint, send_attach_input, send_attach_resize};\n   30│use spt_daemon::effect::{Minter, MintedOp};\n   31│use spt_daemon::brain::{now_ms, Brain, BrokerEvent};\n   32│use spt_daemon::msg::decode_bytes;\n     │...\n 1121│    // (a) STATUS-GATE (REQ-HAZARD-RC-ATTACH-FAILFAST): a perch with an EXPLICIT\n*1122│    // `status=offline` is not attachable — short-circuit with clean guidance\n 1123│    // [impl->REQ-HAZARD-RC-ATTACH-FAILFAST]\n 1124│    // instead of the broker dance that would block on a dead session (the\n 1125│    // infinite-blank hang). The B2 pull-reconcile / `endpoint stop` stamp a dead\n     │...\n 1429│        }\n*1430│        None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n*1431│            crate::wansend::OwnerDial::Dialed { conn_id, node } => {\n 1432│                remote_node = Some(node);\n 1433│                // session_id is a placeholder the owning node ignores — it\n 1434│                // resolves the endpoint to its OWN session id server-side.\n     │...\n 1436│            }\n*1437│            crate::wansend::OwnerDial::NotFound => {\n 1438│                return Err(EstablishFail::NoTarget(format!(\n 1439│                    \"No live session for '{endpoint_id}' — not running on this node \\\n 1440│                     or any visible subnet.\"\n     │...\n 1442│            }\n*1443│            crate::wansend::OwnerDial::Ambiguous(msg) => {\n 1444│                return Err(EstablishFail::NoTarget(msg))\n 1445│            }\n*1446│            crate::wansend::OwnerDial::Unreachable { node, detail } => {\n 1447│                return Err(EstablishFail::NoTarget(format!(\n 1448│                    \"'{endpoint_id}' is on {node}, but it is not reachable ({detail}).\"\n 1449│                )))\n     │...\n 1475│                    // Remote: re-resolve + re-dial the owning node.\n*1476│                    None => match crate::wansend::resolve_and_dial_owner(&mut brain, endpoint_id) {\n*1477│                        crate::wansend::OwnerDial::Dialed { conn_id, .. } => conn_id,\n 1478│                        _ => {\n 1479│                            return Err(std::io::Error::other(format!(\n 1480│                                \"'{endpoint_id}' is no longer reachable\"\n     │...\n 1485│            };\n*1486│            request_attach_endpoint(&mut brain, cid, session_id, 0, op, intent, req_endpoint)\n 1487│        },\n 1488│    )\n 1489│    .map_err(|e| EstablishFail::Error(format!(\"attach request: {e}\")))?;\n### wansend.rs\n   85│#[derive(Debug)]\n  *86│pub enum OwnerDial {\n   87│    /// The owning node was resolved and dialed; `conn_id` is the live QUIC conn,\n   88│    /// `node` its display name (label or key prefix) for any status copy.\n   89│    Dialed { conn_id: u64, node: String },\n     │...\n  115│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n *116│pub fn resolve_and_dial_owner(brain: &mut Brain, endpoint: &str) -> OwnerDial {\n  117│    let own_hex = match spt_store::nodeid::load_or_create() {\n  118│        Ok(id) => id.public_key().to_hex(),\n  119│        Err(e) => {\n *120│            return OwnerDial::Unreachable {\n  121│                node: \"?\".to_string(),\n  122│                detail: format!(\"node identity: {e}\"),\n  123│            }\n     │...\n  128│        Err(e) => {\n *129│            return OwnerDial::Unreachable {\n  130│                node: \"?\".to_string(),\n  131│                detail: format!(\"bad address: {e:?}\"),\n  132│            }\n     │...\n  147│        Resolution::Resolved { instance, .. } => instance,\n *148│        Resolution::Ambiguous(a) => return OwnerDial::Ambiguous(render_refusal(&address.id, &a, &regs)),\n *149│        Resolution::NotFound => return OwnerDial::NotFound,\n  150│    };\n  151│    if instance.node == own_hex {\n  152│        // The registry says WE own it, but the caller already missed it in the\n  153│        // local session scan — a local staleness with no remote to dial.\n *154│        return OwnerDial::NotFound;\n  155│    }\n  156│    let node = node_display(&instance.node, &regs);\n  157│    let Some(addr) = seed_first_addr(&instance.node) else {\n *158│        return OwnerDial::Unreachable {\n  159│            node,\n  160│            detail: \"no dialable address\".to_string(),\n  161│        };\n     │...\n  163│    match brain.net_dial(addr, None) {\n *164│        Ok(c) => OwnerDial::Dialed {\n  165│            conn_id: c.conn_id,\n  166│            node,\n  167│        },\n *168│        Err(e) => OwnerDial::Unreachable {\n  169│            node,\n  170│            detail: e.to_string(),\n  171│        },\n     │...\n 1334│    /// Write a registry snapshot to the canonical on-disk path\n*1335│    /// [`resolve_and_dial_owner`] reads via [`load_snapshots`] — the production\n 1336│    /// disk path, not an injected map.\n 1337│    fn write_snapshot(subnet: &str, id: &str, node: &str) {\n 1338│        let dir = perch::identity_dir().join(\"registry\");\n     │...\n 1354│    #[test]\n*1355│    fn resolve_and_dial_owner_is_honest_on_resolution_misses() {\n 1356│        let _home = isolated_home();\n 1357│        let dir = tempfile::tempdir().unwrap();\n 1358│        let name = format!(\"spt-rcresolve-{}.sock\", std::process::id());\n     │...\n 1363│        assert!(matches!(\n*1364│            resolve_and_dial_owner(&mut a, \"ghost\"),\n*1365│            OwnerDial::NotFound\n 1366│        ));\n 1367│\n 1368│        // Our own node hosts it (registry staleness) → NotFound, no self-dial.\n     │...\n 1374│        assert!(matches!(\n*1375│            resolve_and_dial_owner(&mut a, \"mine\"),\n*1376│            OwnerDial::NotFound\n 1377│        ));\n 1378│\n 1379│        // Two subnets hold it → Ambiguous (refuse-and-qualify), never a guess.\n     │...\n 1382│        assert!(matches!(\n*1383│            resolve_and_dial_owner(&mut a, \"twin\"),\n*1384│            OwnerDial::Ambiguous(_)\n 1385│        ));\n 1386│    }\n 1387│\n     │...\n 1396│    #[test]\n*1397│    fn resolve_and_dial_owner_dials_the_resolved_node() {\n 1398│        use spt_store::peeraddrs::{peer_addrs_file, PeerAddrStore};\n 1399│        let _home = isolated_home();\n 1400│        let dir = tempfile::tempdir().unwrap();\n     │...\n 1420│\n*1421│        match resolve_and_dial_owner(&mut a, \"ling-remote\") {\n\n## spt-daemon/src/\n### attach.rs\n 560│) -> io::Result<u64> {\n*561│    request_attach_endpoint(brain, conn_id, session_id, from_seq, open_op, intent, None)\n 562│}\n 563│\n 564│/// Like [`request_attach`], but for the cross-node leg (#4,\n    │...\n 571│// [impl->REQ-RC-CROSS-NODE-ATTACH]\n*572│pub fn request_attach_endpoint(\n 573│    brain: &mut Brain,\n 574│    conn_id: u64,\n 575│    session_id: u64,"},"isError":false,"timestamp":1784197467248}}
{"type":"message","id":"abe44e72","parentId":"c7598dc6","timestamp":"2026-07-16T10:24:37.838Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reviewing cmd_send and WAN receive code**"},{"type":"toolCall","id":"call_jYxvgRnoEI92kN9CUhMHqCda|fc_017bff223290b7ba016a58b165cae8819a93e2172a66fadc7b","name":"read","arguments":{"i":"Reading local send handler","path":"crates/spt/src/cli.rs","selector":"6420-6605"}},{"type":"toolCall","id":"call_b6OzD0svJhUs4i9dlQXGXsxV|fc_017bff223290b7ba016a58b165caf8819aa5b0a0f0f347cc36","name":"read","arguments":{"i":"Reading RC establish logic","path":"crates/spt/src/rc.rs","selector":"1080-1165,1390-1505"}},{"type":"toolCall","id":"call_OKhjNEzTWSyr9znMDRRqGWBJ|fc_017bff223290b7ba016a58b165cb00819ab29dd126dbf10449","name":"read","arguments":{"i":"Reading WAN receive funnel","path":"crates/spt-daemon/src/wan.rs","selector":"1-120,240-385"}},{"type":"toolCall","id":"call_PWWBWBYNxehdMo8n12Bb6ylI|fc_017bff223290b7ba016a58b165cb04819a822a5e9f367f31c5","name":"read","arguments":{"i":"Reading local advertisement mapping","path":"crates/spt-daemon/src/registryhost.rs","selector":"330-505,790-860"}},{"type":"toolCall","id":"call_KRfIKWT6XZ2MN43kkpG76Dpv|fc_017bff223290b7ba016a58b165cb0c819a93fb02db9e4ffe39","name":"read","arguments":{"i":"Reading address registry implementation","path":"crates/spt-store/src/registry.rs","selector":"1-145,230-295"}},{"type":"toolCall","id":"call_xBzEnykSRMm7zzntTR5BWg2x|fc_017bff223290b7ba016a58b165cb10819ab9638cce7aacc17f","name":"read","arguments":{"i":"Reading relay drain implementation","path":"crates/spt-daemon/src/relay.rs","selector":"1-110"}},{"type":"toolCall","id":"call_yeZ6sFzrw2Rrbeot3TeXpsHv|fc_017bff223290b7ba016a58b165cb14819aa4af32a4bbbca276","name":"read","arguments":{"i":"Reading spool claim implementation","path":"crates/spt-store/src/spool.rs","selector":"1-150,300-450,680-870"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":22626,"output":323,"cacheRead":141824,"cacheWrite":0,"totalTokens":164773,"reasoningTokens":31,"cost":{"input":0.11313000000000001,"output":0.00969,"cacheRead":0.070912,"cacheWrite":0,"total":0.19373200000000002}},"stopReason":"toolUse","timestamp":1784197467302,"responseId":"resp_017bff223290b7ba016a58b15b9a94819a9812995d4f674bad","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b15ff870819aacb36c8975fb0268","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLFlyVKHafhEFN1fIeZkAPEIkcpzSeMHKQOLM-bO595HezQmkDc1pYlDfXj5yt50A8x6EnbqZAp-VQp353VS1_GDuY4fQ3rQDCUYCCRswB6Au_9GBVTW7cMDt3eUyOfUTkk39NbYV5empK4Ky0yvfHo_nCCu1wf9fF1I7-danHYPQQYQ-nkMXD1ZYZ1j-CXeOPRhJiVkjYCRQrkKlot2bwFN30FnjrAn9tF34X-Csfi2i70bCjM94crvryPoh7H9v2E4CHcZWwtOd9MCWvLJEUxBii7QaP_i4gLC_PQmN60mHYXCGrxEP-Es1W5_GsF9bS7sKPo-7a6lBuRCTDkwg1ca1WQBSQw7q75lxImuUgrIJh78GKnfiEUZucun73lZKoRBlYCIdHRpHisnrnUHcvLKKP5IVjzRIolFE-dhuzVWmzvWASx3Q6zghCu7ltqBeF9g8vVZdRC14V32Rw9HHt1x9lwvjGCODr9vFYhTPFU0c2oG7n7zuDD8Ir8SKosAYlKIVdlz1JZzxR_x5xn9C2S3uAoi3CJZlFNjJeJceGfCo_90JiMSAPmbXLSKkkc4s0s-HjOzRTMTwBONi8p1CewqS2XzoQ-27injPsAys31tvqqSi_Ge1rHVLq5TKM9QX2JjO4G8zkJVZAAvcBhl3_1xK4ppfv-7GCgQ9VHX7bxqCKVr80iE3eIxL2w_Piu3LHDuH32XiwzlBIFmPPYPhGCnEZFI9Bp83mLZCf2_fRBGwFGjQJ4oDGl6OIRk4AVB0URczgkwtaJvo0yZ8en4qYCOaAjLyOyEHQ7sJEuviyglQzqj_IH6s8FvDPDwq4bvUK8VVIjecdFOoN3qlZSEbLWMmpLwDkc9TCLZ8jwSh-DMJuTtq3xPjpFr4GOJF1_m23_Y6jOjbusURXINF7Jh8HbEAJsb8ZnaUb6xXu7AslYfIy3zI6Lj-Day7hK61LtFMpnD3ZVcX665tu2t4HIe1WqAYOz1c8L7FfIg6hozF-32v-Uv0VDoCcvkxrRcbUVwPUaEoYuxEY92uE5TKJrLiY8GCZCLXwAHxp2fGhoMjhML5Vk=","summary":[{"type":"summary_text","text":"**Reviewing cmd_send and WAN receive code**"}]},{"id":"fc_017bff223290b7ba016a58b165cae8819a93e2172a66fadc7b","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading local send handler\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"6420-6605\"}","call_id":"call_jYxvgRnoEI92kN9CUhMHqCda","name":"read"},{"id":"fc_017bff223290b7ba016a58b165caf8819aa5b0a0f0f347cc36","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading RC establish logic\",\"path\":\"crates/spt/src/rc.rs\",\"selector\":\"1080-1165,1390-1505\"}","call_id":"call_b6OzD0svJhUs4i9dlQXGXsxV","name":"read"},{"id":"fc_017bff223290b7ba016a58b165cb00819ab29dd126dbf10449","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading WAN receive funnel\",\"path\":\"crates/spt-daemon/src/wan.rs\",\"selector\":\"1-120,240-385\"}","call_id":"call_OKhjNEzTWSyr9znMDRRqGWBJ","name":"read"},{"id":"fc_017bff223290b7ba016a58b165cb04819a822a5e9f367f31c5","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading local advertisement mapping\",\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"selector\":\"330-505,790-860\"}","call_id":"call_PWWBWBYNxehdMo8n12Bb6ylI","name":"read"},{"id":"fc_017bff223290b7ba016a58b165cb0c819a93fb02db9e4ffe39","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading address registry implementation\",\"path\":\"crates/spt-store/src/registry.rs\",\"selector\":\"1-145,230-295\"}","call_id":"call_KRfIKWT6XZ2MN43kkpG76Dpv","name":"read"},{"id":"fc_017bff223290b7ba016a58b165cb10819ab9638cce7aacc17f","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading relay drain implementation\",\"path\":\"crates/spt-daemon/src/relay.rs\",\"selector\":\"1-110\"}","call_id":"call_xBzEnykSRMm7zzntTR5BWg2x","name":"read"},{"id":"fc_017bff223290b7ba016a58b165cb14819aa4af32a4bbbca276","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading spool claim implementation\",\"path\":\"crates/spt-store/src/spool.rs\",\"selector\":\"1-150,300-450,680-870\"}","call_id":"call_yeZ6sFzrw2Rrbeot3TeXpsHv","name":"read"}]},"duration":10535.299699999392,"ttft":4642.5633999994025,"contextSnapshot":{"promptTokens":164450,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jYxvgRnoEI92kN9CUhMHqCda|fc_017bff223290b7ba016a58b165cae8819a93e2172a66fadc7b","toolName":"read","startedAt":"2026-07-16T10:24:37.839Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading local send handler"},"id":"2b8f6dc4","parentId":"abe44e72","timestamp":"2026-07-16T10:24:37.839Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_b6OzD0svJhUs4i9dlQXGXsxV|fc_017bff223290b7ba016a58b165caf8819aa5b0a0f0f347cc36","toolName":"read","startedAt":"2026-07-16T10:24:37.839Z","args":{"path":"crates/spt/src/rc.rs"},"intent":"Reading RC establish logic"},"id":"48241169","parentId":"2b8f6dc4","timestamp":"2026-07-16T10:24:37.839Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OKhjNEzTWSyr9znMDRRqGWBJ|fc_017bff223290b7ba016a58b165cb00819ab29dd126dbf10449","toolName":"read","startedAt":"2026-07-16T10:24:37.839Z","args":{"path":"crates/spt-daemon/src/wan.rs"},"intent":"Reading WAN receive funnel"},"id":"e46a32a8","parentId":"48241169","timestamp":"2026-07-16T10:24:37.839Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_PWWBWBYNxehdMo8n12Bb6ylI|fc_017bff223290b7ba016a58b165cb04819a822a5e9f367f31c5","toolName":"read","startedAt":"2026-07-16T10:24:37.839Z","args":{"path":"crates/spt-daemon/src/registryhost.rs"},"intent":"Reading local advertisement mapping"},"id":"e0349c2a","parentId":"e46a32a8","timestamp":"2026-07-16T10:24:37.839Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_KRfIKWT6XZ2MN43kkpG76Dpv|fc_017bff223290b7ba016a58b165cb0c819a93fb02db9e4ffe39","toolName":"read","startedAt":"2026-07-16T10:24:37.839Z","args":{"path":"crates/spt-store/src/registry.rs"},"intent":"Reading address registry implementation"},"id":"03acf278","parentId":"e0349c2a","timestamp":"2026-07-16T10:24:37.839Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_xBzEnykSRMm7zzntTR5BWg2x|fc_017bff223290b7ba016a58b165cb10819ab9638cce7aacc17f","toolName":"read","startedAt":"2026-07-16T10:24:37.839Z","args":{"path":"crates/spt-daemon/src/relay.rs"},"intent":"Reading relay drain implementation"},"id":"11e94716","parentId":"03acf278","timestamp":"2026-07-16T10:24:37.839Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_yeZ6sFzrw2Rrbeot3TeXpsHv|fc_017bff223290b7ba016a58b165cb14819aa4af32a4bbbca276","toolName":"read","startedAt":"2026-07-16T10:24:37.839Z","args":{"path":"crates/spt-store/src/spool.rs"},"intent":"Reading spool claim implementation"},"id":"a48f6527","parentId":"11e94716","timestamp":"2026-07-16T10:24:37.839Z"}
{"type":"message","id":"194265af","parentId":"a48f6527","timestamp":"2026-07-16T10:24:37.865Z","message":{"role":"toolResult","toolCallId":"call_b6OzD0svJhUs4i9dlQXGXsxV|fc_017bff223290b7ba016a58b165caf8819aa5b0a0f0f347cc36","toolName":"read","content":[{"type":"text","text":"/// 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…\n    };\n…\n}\n…\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"}],"details":{"displayContent":{"text":"/// 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…\n    };\n…\n}\n…\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","startLine":1080,"lineNumbers":[1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,null,1182,null,1315,null,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\rc.rs"}}},"isError":false,"timestamp":1784197477865}}
{"type":"message","id":"2cfb2a21","parentId":"194265af","timestamp":"2026-07-16T10:24:37.865Z","message":{"role":"toolResult","toolCallId":"call_KRfIKWT6XZ2MN43kkpG76Dpv|fc_017bff223290b7ba016a58b165cb0c819a93fb02db9e4ffe39","toolName":"read","content":[{"type":"text","text":"//! SQLite registry for agent address discovery — `<owlery>/.registry`.\n//!\n//! Schema + surface copied verbatim from sister `registry.rs` (ADR-0001). Maps\n//! an agent id → its TCP address + owning pid. This is the **local** registry\n//! (one machine's perches); cross-node subnet resolution (REQ-INST-7 full) is\n//! M4. M0 delivers local persistence + the stale-entry invariant only.\n//!\n//! ## Stale-entry invariant (KNOWN-HAZARDS 4.3, REQ-HAZARD-REGISTRY-STALE-CLEAN)\n//! A registered pid can die without unregistering (crash, kill). Resolving its\n//! stale address would deliver to a dead/wrong listener. So [`clean_stale_entries`]\n//! drops dead-pid rows, and [`resolve_address`] cleans **before** lookup. The\n//! clean step is best-effort: a cleanup failure never hard-fails resolution\n//! (the spool is the safe fallback on a TCP miss).\n\nuse rusqlite::{params, Connection};\nuse std::path::Path;\n\nuse crate::liveness;\n\n/// Open (creating if needed) the registry DB at `<owlery>/.registry`.\nfn open_registry(owlery: &Path) -> rusqlite::Result<Connection> {\n    // [impl->REQ-HAZARD-REGISTRY-DIR-CREATE] SQLite creates the FILE but never\n    // the parent DIR: on a fresh home (first boot, fresh CI workdir) a registry\n    // op that races ahead of any perch-creating op fails SQLITE_CANTOPEN\n    // (\"unable to open database file\"). Ensure the owlery exists, best-effort —\n    // if creation truly fails, Connection::open surfaces the real error (4.9).\n    let _ = std::fs::create_dir_all(owlery);\n    let db_path = owlery.join(\".registry\");\n    let conn = Connection::open(&db_path)?;\n    // [impl->REQ-HAZARD-REGISTRY-CONCURRENT] concurrency-safe pragmas (busy_timeout\n    // first, then WAL-with-retry) so concurrent ReadyAgent registrations don't\n    // fail with \"database is locked\".\n    crate::db::tune_connection(&conn)?;\n    conn.execute_batch(\n        \"CREATE TABLE IF NOT EXISTS agents (\n            id TEXT PRIMARY KEY,\n            address TEXT NOT NULL,\n            pid INTEGER NOT NULL,\n            started TEXT NOT NULL\n        )\",\n    )?;\n    Ok(conn)\n}\n\nfn now_stamp() -> String {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_secs())\n        .unwrap_or(0)\n        .to_string()\n}\n\n/// Register (or replace) an agent's TCP address under the **current** process's\n/// pid.\n// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\npub fn register_address(\n    id: &str,\n    addr: &std::net::SocketAddr,\n    owlery: &Path,\n) -> rusqlite::Result<()> {\n    register_address_with_pid(id, addr, std::process::id(), owlery)\n}\n\n/// Register (or replace) an agent's address under an explicit owning `pid`\n/// (e.g. the daemon registering on behalf of a hosted endpoint process).\n// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\npub fn register_address_with_pid(\n    id: &str,\n    addr: &std::net::SocketAddr,\n    pid: u32,\n    owlery: &Path,\n) -> rusqlite::Result<()> {\n    let conn = open_registry(owlery)?;\n    conn.execute(\n        \"INSERT OR REPLACE INTO agents (id, address, pid, started) VALUES (?1, ?2, ?3, ?4)\",\n        params![id, addr.to_string(), pid, now_stamp()],\n    )?;\n    Ok(())\n}\n\n/// Look up an agent's TCP address by id (no staleness check). `None` on miss or\n/// any DB error — never hard-fails.\npub fn lookup_address(id: &str, owlery: &Path) -> Option<String> {\n    let conn = open_registry(owlery).ok()?;\n    conn.query_row(\n        \"SELECT address FROM agents WHERE id = ?1\",\n        params![id],\n        |row| row.get(0),\n    )\n    .ok()\n}\n\n/// Look up an agent's owning pid by id. `None` on miss / DB error.\npub fn lookup_pid(id: &str, owlery: &Path) -> Option<u32> {\n    let conn = open_registry(owlery).ok()?;\n    conn.query_row(\"SELECT pid FROM agents WHERE id = ?1\", params![id], |row| {\n        row.get::<_, u32>(0)\n    })\n    .ok()\n}\n\n/// Remove an agent's entry.\npub fn unregister_address(id: &str, owlery: &Path) -> rusqlite::Result<()> {\n    let conn = open_registry(owlery)?;\n    conn.execute(\"DELETE FROM agents WHERE id = ?1\", params![id])?;\n    Ok(())\n}\n\n/// Remove every entry whose owning endpoint is no longer alive. Routes through\n/// the single liveness resolver ([`liveness::is_registry_entry_alive`]): external\n/// rows pid-probe the registered pid exactly as before; **daemon-hosted** rows\n/// key on the endpoint's `status` instead (2.5) — so an offline hosted endpoint\n/// is cleaned even though the shared daemon pid is alive, and an online one\n/// survives a dead ephemeral pid.\n// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\npub fn clean_stale_entries(owlery: &Path) -> rusqlite::Result<()> {\n    let conn = open_registry(owlery)?;\n    let mut stmt = conn.prepare(\"SELECT id, pid FROM agents\")?;\n    let stale_ids: Vec<String> = stmt\n        .query_map([], |row| {\n            Ok((row.get::<_, String>(0)?, row.get::<_, u32>(1)?))\n        })?\n        .filter_map(|r| r.ok())\n        .filter(|(id, pid)| !liveness::is_registry_entry_alive(id, *pid, owlery))\n        .map(|(id, _)| id)\n        .collect();\n    for id in &stale_ids {\n        conn.execute(\"DELETE FROM agents WHERE id = ?1\", params![id])?;\n    }\n    Ok(())\n}\n\n/// Resolve an agent's address **safely**: clean stale (dead-pid) entries first,\n/// then look up. The clean step is best-effort — a cleanup error never blocks\n/// resolution (KNOWN-HAZARDS 4.3: degrade to fallback, never hard-fail). This\n/// is the entry point callers should use for delivery routing.\n// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\n// [impl->REQ-INST-7] partial: LOCAL bare-id → address resolution; cross-node\n// subnet resolution is M4 (this req stays inactive until then).\npub fn resolve_address(id: &str, owlery: &Path) -> Option<String> {\n    let _ = clean_stale_entries(owlery); // best-effort; ignore failure\n    lookup_address(id, owlery)\n}\n\n…\n    // its `status`, not the (shared, live) daemon pid: an `offline` hosted\n    // endpoint registered under THIS live process is still swept, while an\n    // external row under the same live pid survives.\n    #[test]\n    fn clean_keys_on_status_for_daemon_hosted_rows() {\n        use crate::info::{set_status, write_info, InfoJson};\n        use crate::liveness::STATUS_OFFLINE;\n        use crate::perch::{resolve_perch_path_in, ParentHint};\n\n        let d = tempfile::tempdir().unwrap();\n        let owlery = d.path();\n        let live = std::process::id();\n\n        // Daemon-hosted, offline, registered under the live daemon pid.\n        let hosted = resolve_perch_path_in(owlery, \"hosted\", ParentHint::Infer);\n        std::fs::create_dir_all(&hosted).unwrap();\n        write_info(\n            &hosted,\n            &InfoJson::new(\"hosted\", \"t\", live, \"sid\", \"psyche\"),\n        )\n        .unwrap();\n        set_status(&hosted, STATUS_OFFLINE).unwrap();\n        register_address_with_pid(\"hosted\", &addr(9100), live, owlery).unwrap();\n\n        // External (no status), same live pid — must survive.\n        register_address_with_pid(\"ext\", &addr(9101), live, owlery).unwrap();\n\n        clean_stale_entries(owlery).unwrap();\n\n        assert_eq!(\n            lookup_address(\"hosted\", owlery),\n            None,\n            \"offline hosted row swept despite live daemon pid\"\n        );\n        assert_eq!(\n            lookup_address(\"ext\", owlery),\n            Some(\"127.0.0.1:9101\".to_string()),\n            \"external live-pid row survives\"\n        );\n    }\n\n    // [unit->REQ-HAZARD-REGISTRY-STALE-CLEAN] resolve_address cleans stale\n    // BEFORE lookup: a dead entry never resolves; a live one does.\n    #[test]\n    fn resolve_cleans_before_lookup() {\n        let d = tempfile::tempdir().unwrap();\n        register_address_with_pid(\"dead\", &addr(9003), dead_pid(), d.path()).unwrap();\n        register_address_with_pid(\"live\", &addr(9004), std::process::id(), d.path()).unwrap();\n\n        assert_eq!(\n            resolve_address(\"dead\", d.path()),\n            None,\n            \"stale resolved → None, not a dead address\"\n        );\n        assert_eq!(\n            resolve_address(\"live\", d.path()),\n            Some(\"127.0.0.1:9004\".to_string())\n        );\n    }\n\n    // [unit->REQ-HAZARD-REGISTRY-STALE-CLEAN] resolve never hard-fails on a\n    // missing/empty registry — returns None.\n    #[test]\n    fn resolve_on_empty_registry_is_none_not_error() {\n        let d = tempfile::tempdir().unwrap();\n        assert_eq!(resolve_address(\"anyone\", d.path()), None);\n    }"}],"details":{"displayContent":{"text":"//! SQLite registry for agent address discovery — `<owlery>/.registry`.\n//!\n//! Schema + surface copied verbatim from sister `registry.rs` (ADR-0001). Maps\n//! an agent id → its TCP address + owning pid. This is the **local** registry\n//! (one machine's perches); cross-node subnet resolution (REQ-INST-7 full) is\n//! M4. M0 delivers local persistence + the stale-entry invariant only.\n//!\n//! ## Stale-entry invariant (KNOWN-HAZARDS 4.3, REQ-HAZARD-REGISTRY-STALE-CLEAN)\n//! A registered pid can die without unregistering (crash, kill). Resolving its\n//! stale address would deliver to a dead/wrong listener. So [`clean_stale_entries`]\n//! drops dead-pid rows, and [`resolve_address`] cleans **before** lookup. The\n//! clean step is best-effort: a cleanup failure never hard-fails resolution\n//! (the spool is the safe fallback on a TCP miss).\n\nuse rusqlite::{params, Connection};\nuse std::path::Path;\n\nuse crate::liveness;\n\n/// Open (creating if needed) the registry DB at `<owlery>/.registry`.\nfn open_registry(owlery: &Path) -> rusqlite::Result<Connection> {\n    // [impl->REQ-HAZARD-REGISTRY-DIR-CREATE] SQLite creates the FILE but never\n    // the parent DIR: on a fresh home (first boot, fresh CI workdir) a registry\n    // op that races ahead of any perch-creating op fails SQLITE_CANTOPEN\n    // (\"unable to open database file\"). Ensure the owlery exists, best-effort —\n    // if creation truly fails, Connection::open surfaces the real error (4.9).\n    let _ = std::fs::create_dir_all(owlery);\n    let db_path = owlery.join(\".registry\");\n    let conn = Connection::open(&db_path)?;\n    // [impl->REQ-HAZARD-REGISTRY-CONCURRENT] concurrency-safe pragmas (busy_timeout\n    // first, then WAL-with-retry) so concurrent ReadyAgent registrations don't\n    // fail with \"database is locked\".\n    crate::db::tune_connection(&conn)?;\n    conn.execute_batch(\n        \"CREATE TABLE IF NOT EXISTS agents (\n            id TEXT PRIMARY KEY,\n            address TEXT NOT NULL,\n            pid INTEGER NOT NULL,\n            started TEXT NOT NULL\n        )\",\n    )?;\n    Ok(conn)\n}\n\nfn now_stamp() -> String {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_secs())\n        .unwrap_or(0)\n        .to_string()\n}\n\n/// Register (or replace) an agent's TCP address under the **current** process's\n/// pid.\n// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\npub fn register_address(\n    id: &str,\n    addr: &std::net::SocketAddr,\n    owlery: &Path,\n) -> rusqlite::Result<()> {\n    register_address_with_pid(id, addr, std::process::id(), owlery)\n}\n\n/// Register (or replace) an agent's address under an explicit owning `pid`\n/// (e.g. the daemon registering on behalf of a hosted endpoint process).\n// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\npub fn register_address_with_pid(\n    id: &str,\n    addr: &std::net::SocketAddr,\n    pid: u32,\n    owlery: &Path,\n) -> rusqlite::Result<()> {\n    let conn = open_registry(owlery)?;\n    conn.execute(\n        \"INSERT OR REPLACE INTO agents (id, address, pid, started) VALUES (?1, ?2, ?3, ?4)\",\n        params![id, addr.to_string(), pid, now_stamp()],\n    )?;\n    Ok(())\n}\n\n/// Look up an agent's TCP address by id (no staleness check). `None` on miss or\n/// any DB error — never hard-fails.\npub fn lookup_address(id: &str, owlery: &Path) -> Option<String> {\n    let conn = open_registry(owlery).ok()?;\n    conn.query_row(\n        \"SELECT address FROM agents WHERE id = ?1\",\n        params![id],\n        |row| row.get(0),\n    )\n    .ok()\n}\n\n/// Look up an agent's owning pid by id. `None` on miss / DB error.\npub fn lookup_pid(id: &str, owlery: &Path) -> Option<u32> {\n    let conn = open_registry(owlery).ok()?;\n    conn.query_row(\"SELECT pid FROM agents WHERE id = ?1\", params![id], |row| {\n        row.get::<_, u32>(0)\n    })\n    .ok()\n}\n\n/// Remove an agent's entry.\npub fn unregister_address(id: &str, owlery: &Path) -> rusqlite::Result<()> {\n    let conn = open_registry(owlery)?;\n    conn.execute(\"DELETE FROM agents WHERE id = ?1\", params![id])?;\n    Ok(())\n}\n\n/// Remove every entry whose owning endpoint is no longer alive. Routes through\n/// the single liveness resolver ([`liveness::is_registry_entry_alive`]): external\n/// rows pid-probe the registered pid exactly as before; **daemon-hosted** rows\n/// key on the endpoint's `status` instead (2.5) — so an offline hosted endpoint\n/// is cleaned even though the shared daemon pid is alive, and an online one\n/// survives a dead ephemeral pid.\n// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\npub fn clean_stale_entries(owlery: &Path) -> rusqlite::Result<()> {\n    let conn = open_registry(owlery)?;\n    let mut stmt = conn.prepare(\"SELECT id, pid FROM agents\")?;\n    let stale_ids: Vec<String> = stmt\n        .query_map([], |row| {\n            Ok((row.get::<_, String>(0)?, row.get::<_, u32>(1)?))\n        })?\n        .filter_map(|r| r.ok())\n        .filter(|(id, pid)| !liveness::is_registry_entry_alive(id, *pid, owlery))\n        .map(|(id, _)| id)\n        .collect();\n    for id in &stale_ids {\n        conn.execute(\"DELETE FROM agents WHERE id = ?1\", params![id])?;\n    }\n    Ok(())\n}\n\n/// Resolve an agent's address **safely**: clean stale (dead-pid) entries first,\n/// then look up. The clean step is best-effort — a cleanup error never blocks\n/// resolution (KNOWN-HAZARDS 4.3: degrade to fallback, never hard-fail). This\n/// is the entry point callers should use for delivery routing.\n// [impl->REQ-HAZARD-REGISTRY-STALE-CLEAN]\n// [impl->REQ-INST-7] partial: LOCAL bare-id → address resolution; cross-node\n// subnet resolution is M4 (this req stays inactive until then).\npub fn resolve_address(id: &str, owlery: &Path) -> Option<String> {\n    let _ = clean_stale_entries(owlery); // best-effort; ignore failure\n    lookup_address(id, owlery)\n}\n\n…\n    // its `status`, not the (shared, live) daemon pid: an `offline` hosted\n    // endpoint registered under THIS live process is still swept, while an\n    // external row under the same live pid survives.\n    #[test]\n    fn clean_keys_on_status_for_daemon_hosted_rows() {\n        use crate::info::{set_status, write_info, InfoJson};\n        use crate::liveness::STATUS_OFFLINE;\n        use crate::perch::{resolve_perch_path_in, ParentHint};\n\n        let d = tempfile::tempdir().unwrap();\n        let owlery = d.path();\n        let live = std::process::id();\n\n        // Daemon-hosted, offline, registered under the live daemon pid.\n        let hosted = resolve_perch_path_in(owlery, \"hosted\", ParentHint::Infer);\n        std::fs::create_dir_all(&hosted).unwrap();\n        write_info(\n            &hosted,\n            &InfoJson::new(\"hosted\", \"t\", live, \"sid\", \"psyche\"),\n        )\n        .unwrap();\n        set_status(&hosted, STATUS_OFFLINE).unwrap();\n        register_address_with_pid(\"hosted\", &addr(9100), live, owlery).unwrap();\n\n        // External (no status), same live pid — must survive.\n        register_address_with_pid(\"ext\", &addr(9101), live, owlery).unwrap();\n\n        clean_stale_entries(owlery).unwrap();\n\n        assert_eq!(\n            lookup_address(\"hosted\", owlery),\n            None,\n            \"offline hosted row swept despite live daemon pid\"\n        );\n        assert_eq!(\n            lookup_address(\"ext\", owlery),\n            Some(\"127.0.0.1:9101\".to_string()),\n            \"external live-pid row survives\"\n        );\n    }\n\n    // [unit->REQ-HAZARD-REGISTRY-STALE-CLEAN] resolve_address cleans stale\n    // BEFORE lookup: a dead entry never resolves; a live one does.\n    #[test]\n    fn resolve_cleans_before_lookup() {\n        let d = tempfile::tempdir().unwrap();\n        register_address_with_pid(\"dead\", &addr(9003), dead_pid(), d.path()).unwrap();\n        register_address_with_pid(\"live\", &addr(9004), std::process::id(), d.path()).unwrap();\n\n        assert_eq!(\n            resolve_address(\"dead\", d.path()),\n            None,\n            \"stale resolved → None, not a dead address\"\n        );\n        assert_eq!(\n            resolve_address(\"live\", d.path()),\n            Some(\"127.0.0.1:9004\".to_string())\n        );\n    }\n\n    // [unit->REQ-HAZARD-REGISTRY-STALE-CLEAN] resolve never hard-fails on a\n    // missing/empty registry — returns None.\n    #[test]\n    fn resolve_on_empty_registry_is_none_not_error() {\n        let d = tempfile::tempdir().unwrap();\n        assert_eq!(resolve_address(\"anyone\", d.path()), None);\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,null,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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\registry.rs"}}},"isError":false,"timestamp":1784197477865}}
{"type":"message","id":"f28e98dc","parentId":"2cfb2a21","timestamp":"2026-07-16T10:24:37.884Z","message":{"role":"toolResult","toolCallId":"call_PWWBWBYNxehdMo8n12Bb6ylI|fc_017bff223290b7ba016a58b165cb04819a822a5e9f367f31c5","toolName":"read","content":[{"type":"text","text":"impl RegistryHost {\n…\n    pub fn apply_node_labels(\n…\n    ) -> Vec<RegistryApplyVerdict> {\n…\n        if admitted_any {\n            self.heard\n                .lock()\n                .unwrap()\n                .insert(origin_node.to_string(), wall_ms());\n        }\n        self.write_snapshots();\n        verdicts\n    }\n\n    /// Scan the owlery and (re-)advertise every self perch into every member\n    /// subnet it is visible in, each row stamped with a fresh epoch. Returns\n    /// the updates to push to peers (the node's own rows only — peers' rows\n    /// reconverge from their own re-advertisement, no transitive gossip in\n    /// v1: trust is pairwise per subnet, a relayed third-party row would\n    /// carry an origin the receiver can't gate honestly).\n    // [impl->REQ-INST-7]\n    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {\n        let subnets = SubnetStore::load();\n        let mut vis = VisibilityStore::load();\n        // First-join home adoption (D9-5, REQ-INST-15): within one\n        // advertisement cadence of the node's first join (a pair lands the\n        // subnet record; no pairing-protocol threading needed), unhomed Self\n        // perches adopt the sole subnet + seed their sync scope. Strictly\n        // adopt-if-unset; a multi-subnet store adopts nothing.\n        // [impl->REQ-INST-15]\n        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {\n            let _ = vis.save();\n        }\n        let vis = vis;\n        // The node-level resource-blurb seed (D9-4): an endpoint that has not\n        // authored its own blurb advertises this default (config seeds, the\n        // agent refines — CONTEXT §resource advertisement).\n        let node_blurb = crate::config::DaemonConfig::load().resources_blurb;\n        // The node's subnet-broadcast shell adapters (M5-D8c, CONTEXT\n        // §Discovery scope): registered kind=\"shell\" adapters that chose\n        // broadcast=subnet ride every row this node authors — the other-node\n        // discovery arm. Computed once per scan (the registration set is\n        // node-level).\n        // [impl->REQ-EP-5]\n        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())\n            .iter()\n            .filter(|(r, m)| {\n                r.kind == spt_runtime::manifest::AdapterKind::Shell\n                    && m.shell.as_ref().and_then(|s| s.broadcast)\n                        == Some(spt_runtime::manifest::Broadcast::Subnet)\n            })\n            .map(|(r, _)| r.name.clone())\n            .collect();\n        let mut out = Vec::new();\n        let mut regs = self.regs.lock().unwrap();\n        let mut epoch = self.epoch.lock().unwrap();\n\n        // The set of self perches that still EXIST on disk this round — the\n        // ghost-heal predicate below (a LOCAL registry row whose id is absent\n        // here was erased, REQ-HAZARD-ROSTER-GHOST).\n        let live_ids: std::collections::BTreeSet<String> =\n            perch::list_self_perch_ids(owlery).into_iter().collect();\n\n        for id in perch::list_self_perch_ids(owlery) {\n            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n            // Advertisement follows the resting-state machine (D9-2,\n            // REQ-INST-3): a live perch with a resting record advertises its\n            // machine state (active / dormant / suspended); a recordless\n            // (pre-D9-2 / interim) or dead perch keeps the legacy\n            // liveness-derived mapping. Every advertisement stamps a fresh\n            // epoch below, so a transition reaches peers as an epoch-bumped\n            // update — the lease orders it (KH 4.8).\n            // [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]\n            let status = advertised_status(&perch_path);\n            // Endpoint-authored blurb wins; the node seed fills the gap\n            // (both-authored, REQ-INST-14). Rides the same epoch lease as\n            // every instance update — a blurb edit reaches peers as an\n            // ordered, epoch-bumped row.\n            // [impl->REQ-INST-14]\n            let info = spt_store::info::read_info(&perch_path);\n            let resources = info\n                .as_ref()\n                .and_then(|i| i.resources.clone())\n                .or_else(|| node_blurb.clone());\n            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat\n            // stamp (`lifecycle::touch_active` — Active seats only; a resting\n            // seat never stamps) rides the row like the blurb does — same\n            // epoch lease, same visibility gate, no new channel. Subnet-wide\n            // most-recently-active is a projection of these rows (D6b).\n            // [impl->REQ-PRES-1]\n            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the\n            // info.json `state` IS the bound endpoint type\n            // (`EndpointType::as_tag` — `\"gateway\"` for a Gateway surface).\n            // A node advertises ONLY its own endpoints' types; the cross-node\n            // user-msg gate reads this at the receive funnel (T3). A recordless\n            // perch advertises no type → the funnel fail-closes to a re-stamp.\n            // [impl->REQ-MSG-6]\n            let endpoint_type = info.as_ref().map(|i| i.state.clone());\n            // The unified subnet-display parity datums (REQ-SUBNET-DISPLAY-PARITY):\n            // the SAME local facts the picker's `local_rows` reads off info.json,\n            // now GOSSIPED so a remote viewer renders bound/controlled/harness-only\n            // identically instead of reducing every remote row to plain green.\n            //  - bound: a bound live broker session (`is_perch_alive` is bound-gated,\n            //    so an UNBOUND perch reads false — exactly the fill duality the\n            //    picker keys on).\n            //  - controller_node: the broker-owned `driven_by` (the driving node),\n            //    if any → the remote blue \"controlled\" + `controlled by <node>` pin.\n            //  - harness_only: a LiveAgent with no broker PTY seat — mirrors the\n            //    picker's `controllable != Some(true)` for a `live_agent` (a legacy\n            //    None self-corrects on the next bind, so it advertises harness-only\n            //    until then, matching the local amber).\n            // [impl->REQ-SUBNET-DISPLAY-PARITY]\n            let bound = is_perch_alive(&perch_path);\n            // #3 (REQ-GOSSIP-CONTROLLED-ANY): gossip ANY controller, not just a\n            // remote one. `driven_by` is remote-only (KH 7.15); a LOCAL controller\n            // sets the separate `controlled` flag instead, and its driving node IS\n            // this node — so advertise `self.node_hex`. A remote controller keeps\n            // its own origin. This is what lets a remote viewer see a\n            // locally-controlled endpoint as CONTROLLED, not \"ready to control\".\n            // [impl->REQ-GOSSIP-CONTROLLED-ANY]\n            let controller_node = gossip_controller_node(\n                info.as_ref().and_then(|i| i.driven_by.as_deref()),\n                info.as_ref().is_some_and(|i| i.controlled),\n                &self.node_hex,\n            );\n            let harness_only = info.as_ref().is_some_and(|i| {\n                i.state == \"live_agent\" && i.controllable != Some(true)\n            });\n            // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS): the endpoint's\n            // real harness adapter + its recent projects + the EXPLICIT any-controller\n            // truth, gossiped so `from_resource_row` stops faking remote rows.\n            //  - adapter: the composite `<adapter>[:profile]` off info.json (the picker\n            //    used to show the resources blurb here).\n            //  - recent_projects: sessions.log-derived, owlery-excluded, project IDs only.\n            //  - controlled: the raw any-controller marker (NOT controller_node.is_some();\n            //    a local controller has driven_by=None so that WHO datum undercounts it).\n            // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n            let adapter = info.as_ref().and_then(|i| i.adapter.clone());\n            let controlled = info.as_ref().is_some_and(|i| i.controlled);\n            let recent_projects = recent_projects_for(&perch_path, owlery);\n            let last_active_ms = info.and_then(|i| i.last_active_ms);\n            for sub in &subnets.subnets {\n                let Ok(ep) = epoch.next_epoch() else { continue };\n                let instance = Instance {\n                    node: self.node_hex.clone(),\n                    status,\n                    epoch: ep,\n                    resources: resources.clone(),\n                    last_active_ms,\n                    shell_adapters: shell_adapters.clone(),\n                    // The node label rides every row this node authors\n                    // (REQ-SUBNET-3) — set from the OS hostname at daemon\n                    // startup, so a rename converges by re-advertisement.\n                    // [impl->REQ-SUBNET-3]\n                    node_label: self.node_label.clone(),\n                    // The hashed machine id rides beside it (M8 decision\n                    // 13/22) — the re-pair overwrite anchor.\n                    // [impl->REQ-SUBNET-7]\n                    machine_id: self.machine_id.clone(),\n                    // The endpoint's own type tag (REQ-MSG-6) — read by the\n                    // cross-node user-msg gate, keyed by the proven node.\n                    // [impl->REQ-MSG-6]\n                    endpoint_type: endpoint_type.clone(),\n                    // The unified subnet-display parity datums — gossiped so a\n                    // remote picker renders bound/controlled/harness-only with the\n                    // same fidelity as the home node (REQ-SUBNET-DISPLAY-PARITY).\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\n                    bound,\n                    controller_node: controller_node.clone(),\n                    harness_only,\n                    // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS).\n                    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n                    adapter: adapter.clone(),\n                    recent_projects: recent_projects.clone(),\n                    controlled,\n                };\n                let reg = regs.entry(sub.name.clone()).or_default();\n                match advertise_if_visible(\n                    reg,\n                    &vis,\n                    &sub.name,\n                    sub.hide_new_endpoints,\n…\n                ) {\n…\n                }\n            }\n        }\n…\n    }\n…\n    pub fn consume_repair_evictions(&self) -> usize {\n…\n        if evicted > 0 {\n            self.write_snapshots();\n        }\n        evicted\n    }\n}\n\n/// The [`Status`] this node advertises for one of its OWN perches (the\n/// self-gossip path of [`RegistryHost::advertise_local`]).\n///\n/// - **Bound + alive** (`is_perch_alive`): follow the resting-state machine —\n///   Active / Dormant / Suspended per the perch's resting record (recordless ⇒\n///   Active), the multi-instance routing differentiator (REQ-INST-3).\n/// - **Live but UNBOUND** (`is_perch_unbound`): `is_perch_alive` is bound-gated\n///   so it reads false, but a live broker session exists — the perch is WARM, a\n///   valid routing fallback → `Dormant` (the picker maps Dormant→online; W5\n///   refines the *display* of unbound).\n/// - **Cold** (no live session, not unbound): this node is nonetheless UP — this\n///   very daemon is gossiping the row — so `Suspended` (cold-but-node-up,\n///   resumable-on-wake). NEVER `Dormant` (that requires warm and falsely reads\n///   ONLINE on a remote picker — the bug this fixes) and NEVER `Offline` (a live\n///   node never self-gossips Offline; node-down is only ever remote-inferred via\n///   eviction, and `RestState` has no Offline).\n/// - **Corrupt** (info.json present but destroyed — a hard-reset NUL zero-fill):\n///   `is_perch_alive` now reads false for a corrupt record and `is_perch_unbound`\n///   reads false too (unparseable ⇒ no status), so a corrupt perch lands in the\n///   cold arm → `Suspended`, NEVER Active/Dormant. Without this the daemon\n///   self-gossiped a wiped endpoint as ONLINE forever\n///   (REQ-HAZARD-CORRUPT-PERCH-COHERENCE, counter-39 bug #2).\n// [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]\n// [impl->REQ-HAZARD-CORRUPT-PERCH-COHERENCE]\n// [impl->REQ-EFFECTIVE-INSTANCE-STATE]\nfn advertised_status(perch_path: &Path) -> Status {\n    let intent = crate::resting::read_rest(perch_path).map(|r| r.state);\n    match crate::resting::effective_rest_state(\n        is_perch_alive(perch_path),\n        is_perch_unbound(perch_path),\n        intent,\n    ) {\n        crate::resting::RestState::Active => Status::Active,\n        crate::resting::RestState::Dormant => Status::Dormant,\n        crate::resting::RestState::Suspended => Status::Suspended,\n    }\n}\n\n/// The controlling node to gossip on an Instance (#3, REQ-GOSSIP-CONTROLLED-ANY).\n/// A REMOTE controller keeps its own `driven_by` origin. A LOCAL controller (the\n/// separate `controlled` flag, with `driven_by` = None per KH 7.15) is driven from\n/// THIS node, so it gossips `self_node`. An undriven endpoint gossips `None`. Pure\n/// over its inputs so the any-controller mapping is unit-tested.\n// [impl->REQ-GOSSIP-CONTROLLED-ANY]\nfn gossip_controller_node(\n    driven_by: Option<&str>,\n    controlled: bool,\n    self_node: &str,\n) -> Option<String> {\n    match driven_by {\n        Some(remote) => Some(remote.to_string()),\n        None if controlled => Some(self_node.to_string()),\n        None => None,\n    }\n}\n\n/// Bound on the recent-project IDs a single instance gossips — newest-first, so the\n/// cap keeps the freshest projects. Keeps the wire compact (a long-lived endpoint can\n/// accumulate many cwds); the picker's project pane never needs more.\nconst MAX_GOSSIPED_PROJECTS: usize = 8;\n\n/// The endpoint's recent project IDs newest→oldest for gossip (#4,\n/// REQ-GOSSIP-ADAPTER-PROJECTS): the session ledger's cwds mapped to project IDs,\n/// owlery-internal cwds excluded (a psyche-host session is not a project — mirrors the"}],"details":{"displayContent":{"text":"impl RegistryHost {\n…\n    pub fn apply_node_labels(\n…\n    ) -> Vec<RegistryApplyVerdict> {\n…\n        if admitted_any {\n            self.heard\n                .lock()\n                .unwrap()\n                .insert(origin_node.to_string(), wall_ms());\n        }\n        self.write_snapshots();\n        verdicts\n    }\n\n    /// Scan the owlery and (re-)advertise every self perch into every member\n    /// subnet it is visible in, each row stamped with a fresh epoch. Returns\n    /// the updates to push to peers (the node's own rows only — peers' rows\n    /// reconverge from their own re-advertisement, no transitive gossip in\n    /// v1: trust is pairwise per subnet, a relayed third-party row would\n    /// carry an origin the receiver can't gate honestly).\n    // [impl->REQ-INST-7]\n    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {\n        let subnets = SubnetStore::load();\n        let mut vis = VisibilityStore::load();\n        // First-join home adoption (D9-5, REQ-INST-15): within one\n        // advertisement cadence of the node's first join (a pair lands the\n        // subnet record; no pairing-protocol threading needed), unhomed Self\n        // perches adopt the sole subnet + seed their sync scope. Strictly\n        // adopt-if-unset; a multi-subnet store adopts nothing.\n        // [impl->REQ-INST-15]\n        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {\n            let _ = vis.save();\n        }\n        let vis = vis;\n        // The node-level resource-blurb seed (D9-4): an endpoint that has not\n        // authored its own blurb advertises this default (config seeds, the\n        // agent refines — CONTEXT §resource advertisement).\n        let node_blurb = crate::config::DaemonConfig::load().resources_blurb;\n        // The node's subnet-broadcast shell adapters (M5-D8c, CONTEXT\n        // §Discovery scope): registered kind=\"shell\" adapters that chose\n        // broadcast=subnet ride every row this node authors — the other-node\n        // discovery arm. Computed once per scan (the registration set is\n        // node-level).\n        // [impl->REQ-EP-5]\n        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())\n            .iter()\n            .filter(|(r, m)| {\n                r.kind == spt_runtime::manifest::AdapterKind::Shell\n                    && m.shell.as_ref().and_then(|s| s.broadcast)\n                        == Some(spt_runtime::manifest::Broadcast::Subnet)\n            })\n            .map(|(r, _)| r.name.clone())\n            .collect();\n        let mut out = Vec::new();\n        let mut regs = self.regs.lock().unwrap();\n        let mut epoch = self.epoch.lock().unwrap();\n\n        // The set of self perches that still EXIST on disk this round — the\n        // ghost-heal predicate below (a LOCAL registry row whose id is absent\n        // here was erased, REQ-HAZARD-ROSTER-GHOST).\n        let live_ids: std::collections::BTreeSet<String> =\n            perch::list_self_perch_ids(owlery).into_iter().collect();\n\n        for id in perch::list_self_perch_ids(owlery) {\n            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);\n            // Advertisement follows the resting-state machine (D9-2,\n            // REQ-INST-3): a live perch with a resting record advertises its\n            // machine state (active / dormant / suspended); a recordless\n            // (pre-D9-2 / interim) or dead perch keeps the legacy\n            // liveness-derived mapping. Every advertisement stamps a fresh\n            // epoch below, so a transition reaches peers as an epoch-bumped\n            // update — the lease orders it (KH 4.8).\n            // [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]\n            let status = advertised_status(&perch_path);\n            // Endpoint-authored blurb wins; the node seed fills the gap\n            // (both-authored, REQ-INST-14). Rides the same epoch lease as\n            // every instance update — a blurb edit reaches peers as an\n            // ordered, epoch-bumped row.\n            // [impl->REQ-INST-14]\n            let info = spt_store::info::read_info(&perch_path);\n            let resources = info\n                .as_ref()\n                .and_then(|i| i.resources.clone())\n                .or_else(|| node_blurb.clone());\n            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat\n            // stamp (`lifecycle::touch_active` — Active seats only; a resting\n            // seat never stamps) rides the row like the blurb does — same\n            // epoch lease, same visibility gate, no new channel. Subnet-wide\n            // most-recently-active is a projection of these rows (D6b).\n            // [impl->REQ-PRES-1]\n            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the\n            // info.json `state` IS the bound endpoint type\n            // (`EndpointType::as_tag` — `\"gateway\"` for a Gateway surface).\n            // A node advertises ONLY its own endpoints' types; the cross-node\n            // user-msg gate reads this at the receive funnel (T3). A recordless\n            // perch advertises no type → the funnel fail-closes to a re-stamp.\n            // [impl->REQ-MSG-6]\n            let endpoint_type = info.as_ref().map(|i| i.state.clone());\n            // The unified subnet-display parity datums (REQ-SUBNET-DISPLAY-PARITY):\n            // the SAME local facts the picker's `local_rows` reads off info.json,\n            // now GOSSIPED so a remote viewer renders bound/controlled/harness-only\n            // identically instead of reducing every remote row to plain green.\n            //  - bound: a bound live broker session (`is_perch_alive` is bound-gated,\n            //    so an UNBOUND perch reads false — exactly the fill duality the\n            //    picker keys on).\n            //  - controller_node: the broker-owned `driven_by` (the driving node),\n            //    if any → the remote blue \"controlled\" + `controlled by <node>` pin.\n            //  - harness_only: a LiveAgent with no broker PTY seat — mirrors the\n            //    picker's `controllable != Some(true)` for a `live_agent` (a legacy\n            //    None self-corrects on the next bind, so it advertises harness-only\n            //    until then, matching the local amber).\n            // [impl->REQ-SUBNET-DISPLAY-PARITY]\n            let bound = is_perch_alive(&perch_path);\n            // #3 (REQ-GOSSIP-CONTROLLED-ANY): gossip ANY controller, not just a\n            // remote one. `driven_by` is remote-only (KH 7.15); a LOCAL controller\n            // sets the separate `controlled` flag instead, and its driving node IS\n            // this node — so advertise `self.node_hex`. A remote controller keeps\n            // its own origin. This is what lets a remote viewer see a\n            // locally-controlled endpoint as CONTROLLED, not \"ready to control\".\n            // [impl->REQ-GOSSIP-CONTROLLED-ANY]\n            let controller_node = gossip_controller_node(\n                info.as_ref().and_then(|i| i.driven_by.as_deref()),\n                info.as_ref().is_some_and(|i| i.controlled),\n                &self.node_hex,\n            );\n            let harness_only = info.as_ref().is_some_and(|i| {\n                i.state == \"live_agent\" && i.controllable != Some(true)\n            });\n            // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS): the endpoint's\n            // real harness adapter + its recent projects + the EXPLICIT any-controller\n            // truth, gossiped so `from_resource_row` stops faking remote rows.\n            //  - adapter: the composite `<adapter>[:profile]` off info.json (the picker\n            //    used to show the resources blurb here).\n            //  - recent_projects: sessions.log-derived, owlery-excluded, project IDs only.\n            //  - controlled: the raw any-controller marker (NOT controller_node.is_some();\n            //    a local controller has driven_by=None so that WHO datum undercounts it).\n            // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n            let adapter = info.as_ref().and_then(|i| i.adapter.clone());\n            let controlled = info.as_ref().is_some_and(|i| i.controlled);\n            let recent_projects = recent_projects_for(&perch_path, owlery);\n            let last_active_ms = info.and_then(|i| i.last_active_ms);\n            for sub in &subnets.subnets {\n                let Ok(ep) = epoch.next_epoch() else { continue };\n                let instance = Instance {\n                    node: self.node_hex.clone(),\n                    status,\n                    epoch: ep,\n                    resources: resources.clone(),\n                    last_active_ms,\n                    shell_adapters: shell_adapters.clone(),\n                    // The node label rides every row this node authors\n                    // (REQ-SUBNET-3) — set from the OS hostname at daemon\n                    // startup, so a rename converges by re-advertisement.\n                    // [impl->REQ-SUBNET-3]\n                    node_label: self.node_label.clone(),\n                    // The hashed machine id rides beside it (M8 decision\n                    // 13/22) — the re-pair overwrite anchor.\n                    // [impl->REQ-SUBNET-7]\n                    machine_id: self.machine_id.clone(),\n                    // The endpoint's own type tag (REQ-MSG-6) — read by the\n                    // cross-node user-msg gate, keyed by the proven node.\n                    // [impl->REQ-MSG-6]\n                    endpoint_type: endpoint_type.clone(),\n                    // The unified subnet-display parity datums — gossiped so a\n                    // remote picker renders bound/controlled/harness-only with the\n                    // same fidelity as the home node (REQ-SUBNET-DISPLAY-PARITY).\n                    // [impl->REQ-SUBNET-DISPLAY-PARITY]\n                    bound,\n                    controller_node: controller_node.clone(),\n                    harness_only,\n                    // The #4 de-faking datums (REQ-GOSSIP-ADAPTER-PROJECTS).\n                    // [impl->REQ-GOSSIP-ADAPTER-PROJECTS]\n                    adapter: adapter.clone(),\n                    recent_projects: recent_projects.clone(),\n                    controlled,\n                };\n                let reg = regs.entry(sub.name.clone()).or_default();\n                match advertise_if_visible(\n                    reg,\n                    &vis,\n                    &sub.name,\n                    sub.hide_new_endpoints,\n…\n                ) {\n…\n                }\n            }\n        }\n…\n    }\n…\n    pub fn consume_repair_evictions(&self) -> usize {\n…\n        if evicted > 0 {\n            self.write_snapshots();\n        }\n        evicted\n    }\n}\n\n/// The [`Status`] this node advertises for one of its OWN perches (the\n/// self-gossip path of [`RegistryHost::advertise_local`]).\n///\n/// - **Bound + alive** (`is_perch_alive`): follow the resting-state machine —\n///   Active / Dormant / Suspended per the perch's resting record (recordless ⇒\n///   Active), the multi-instance routing differentiator (REQ-INST-3).\n/// - **Live but UNBOUND** (`is_perch_unbound`): `is_perch_alive` is bound-gated\n///   so it reads false, but a live broker session exists — the perch is WARM, a\n///   valid routing fallback → `Dormant` (the picker maps Dormant→online; W5\n///   refines the *display* of unbound).\n/// - **Cold** (no live session, not unbound): this node is nonetheless UP — this\n///   very daemon is gossiping the row — so `Suspended` (cold-but-node-up,\n///   resumable-on-wake). NEVER `Dormant` (that requires warm and falsely reads\n///   ONLINE on a remote picker — the bug this fixes) and NEVER `Offline` (a live\n///   node never self-gossips Offline; node-down is only ever remote-inferred via\n///   eviction, and `RestState` has no Offline).\n/// - **Corrupt** (info.json present but destroyed — a hard-reset NUL zero-fill):\n///   `is_perch_alive` now reads false for a corrupt record and `is_perch_unbound`\n///   reads false too (unparseable ⇒ no status), so a corrupt perch lands in the\n///   cold arm → `Suspended`, NEVER Active/Dormant. Without this the daemon\n///   self-gossiped a wiped endpoint as ONLINE forever\n///   (REQ-HAZARD-CORRUPT-PERCH-COHERENCE, counter-39 bug #2).\n// [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]\n// [impl->REQ-HAZARD-CORRUPT-PERCH-COHERENCE]\n// [impl->REQ-EFFECTIVE-INSTANCE-STATE]\nfn advertised_status(perch_path: &Path) -> Status {\n    let intent = crate::resting::read_rest(perch_path).map(|r| r.state);\n    match crate::resting::effective_rest_state(\n        is_perch_alive(perch_path),\n        is_perch_unbound(perch_path),\n        intent,\n    ) {\n        crate::resting::RestState::Active => Status::Active,\n        crate::resting::RestState::Dormant => Status::Dormant,\n        crate::resting::RestState::Suspended => Status::Suspended,\n    }\n}\n\n/// The controlling node to gossip on an Instance (#3, REQ-GOSSIP-CONTROLLED-ANY).\n/// A REMOTE controller keeps its own `driven_by` origin. A LOCAL controller (the\n/// separate `controlled` flag, with `driven_by` = None per KH 7.15) is driven from\n/// THIS node, so it gossips `self_node`. An undriven endpoint gossips `None`. Pure\n/// over its inputs so the any-controller mapping is unit-tested.\n// [impl->REQ-GOSSIP-CONTROLLED-ANY]\nfn gossip_controller_node(\n    driven_by: Option<&str>,\n    controlled: bool,\n    self_node: &str,\n) -> Option<String> {\n    match driven_by {\n        Some(remote) => Some(remote.to_string()),\n        None if controlled => Some(self_node.to_string()),\n        None => None,\n    }\n}\n\n/// Bound on the recent-project IDs a single instance gossips — newest-first, so the\n/// cap keeps the freshest projects. Keeps the wire compact (a long-lived endpoint can\n/// accumulate many cwds); the picker's project pane never needs more.\nconst MAX_GOSSIPED_PROJECTS: usize = 8;\n\n/// The endpoint's recent project IDs newest→oldest for gossip (#4,\n/// REQ-GOSSIP-ADAPTER-PROJECTS): the session ledger's cwds mapped to project IDs,\n/// owlery-internal cwds excluded (a psyche-host session is not a project — mirrors the","startLine":122,"lineNumbers":[122,null,300,null,305,null,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,null,508,null,521,522,523,null,606,null,763,null,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\registryhost.rs"}}},"isError":false,"timestamp":1784197477883}}
{"type":"message","id":"a77e0d84","parentId":"f28e98dc","timestamp":"2026-07-16T10:24:37.884Z","message":{"role":"toolResult","toolCallId":"call_OKhjNEzTWSyr9znMDRRqGWBJ|fc_017bff223290b7ba016a58b165cb00819ab29dd126dbf10449","toolName":"read","content":[{"type":"text","text":"//! WAN message receive funnel (M4-D5a) — wire record → access gate → op-id\n//! dedup → the existing local delivery path.\n//!\n//! The sending node resolved the target (D3c policy), dialed, and wrote\n//! [`WanMessage`] NDJSON lines onto a broker-owned QUIC stream (D4). This\n//! module is the receiving node's funnel from a decoded record to the local\n//! REQ-MSG machinery — it adds exactly two things the local path doesn't have,\n//! then delegates:\n//!\n//! 1. **the access gate** ([`crate::access`], ADR-0009): every wire-inbound\n//!    record passes [`access_check`] with the **handshake-proven** origin node\n//!    (the broker stream's `remote_id_hex` — never a payload field,\n//!    REQ-HAZARD-WAN-ORIGIN-AUTH);\n//! 2. **op-id idempotency** (REQ-HAZARD-RESTART-IDEMPOTENT): the wire feed\n//!    replays — a sender's journaled redrive, or this brain restarting and\n//!    resubscribing from an earlier cursor — so delivery dedups on the\n//!    sender-minted durable `op_id`.\n//!\n//! ## Exactly-once boundaries (honest accounting)\n//!\n//! - **Spool path (offline target):** fully exactly-once — the op-id claim and\n//!   the spool row commit in ONE SQLite transaction\n//!   ([`spool::spool_wan_message_at`]); a crash on either side of the write\n//!   replays to the same single row.\n//! - **Live-TCP path:** check → TCP write → claim. A crash in the gap between\n//!   a completed TCP write and the claim re-delivers that op on replay (an\n//!   at-least-once window bounded to one in-flight record; the live listener\n//!   path has no durable receipt to dedup against). Claim-before-write was\n//!   rejected because a crash in ITS gap silently drops the message — for a\n//!   messaging substrate, a rare duplicate beats a rare silent drop. Tracked\n//!   in `docs/FAULT-MATRIX.md`.\n\nuse std::borrow::Cow;\nuse std::path::Path;\n\nuse spt_msg::deliver::{deliver_tcp, perch_exists};\nuse spt_net::net::ndjson;\nuse spt_net::net::registry::{node_label_display, Instance};\nuse spt_net::net::wanmsg::{WanMessage, WanReplyDecoder};\n\nuse crate::brain::{Brain, BrokerEvent};\nuse spt_proto::endpoint::EndpointType;\nuse spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};\nuse spt_store::spool;\n\nuse crate::access::{access_check, classify_inbound, AccessDecision};\nuse crate::registryhost::RegistryHost;\n\n/// Re-stamp a WAN-inbound `user-msg` body down to plain `msg` **unless** the\n/// transport-proven origin is a user-backed endpoint (CONTEXT §Gateway; the\n/// KH 7.5 cross-node enforcement — authority is the proven origin, never the\n/// payload). The honored `user-msg` form is a verbatim typed-envelope body, so\n/// a forged one would otherwise reach the receiver as user-authority; here we\n/// decode it and recompose a plain `msg` envelope, preserving `from` + the inner\n/// body. A non-`user-msg` body (a plain message, any other typed envelope, or a\n/// bare body) is returned unchanged.\n///\n/// `origin_user_backed` is resolved by the caller from the proven origin node\n/// ([`origin_user_backed`]): over WAN, the ONLY provably user-backed origin is a\n/// registry-resolved Gateway endpoint advertised on the proven node (REQ-MSG-6).\n/// An origin that advertises no type, or a non-Gateway type, yields `false` and\n/// the `user-msg` is re-stamped — fail-closed by construction.\n// [impl->REQ-MSG-5]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\nfn restamp_wan_user_msg(body: &str, origin_user_backed: bool) -> Cow<'_, str> {\n    if origin_user_backed {\n        return Cow::Borrowed(body);\n    }\n    match parse_event(body) {\n        Some(ev) if ev.event_type.as_deref() == Some(EVENT_TYPE_USER_MSG) => {\n            Cow::Owned(compose_msg_event(ev.from().unwrap_or(\"\"), &ev.body))\n        }\n        _ => Cow::Borrowed(body),\n    }\n}\n\n/// Resolve whether a WAN-inbound `user-msg`'s origin is provably **user-backed**\n/// (REQ-MSG-6 — CONTEXT §Gateway): among the origin endpoint's advertised\n/// instances, the one hosted on the **QUIC-handshake-proven** `origin_node`\n/// advertises the Gateway type ([`spt_proto::event::GATEWAY_TAG`]). Keyed on the\n/// proven node, NEVER the wire `from` — a record whose `from` claims a gateway\n/// id is inert unless the proven node actually hosts that gateway-typed\n/// endpoint. An absent or non-gateway advertised type → `false`: the fail-closed\n/// re-stamp (an N-1 node that does not yet advertise `endpoint_type` degrades\n/// gracefully to plain `msg`).\n///\n/// This resolves the *advertised* type; it is **not** a forgery defense. The\n/// trust boundary is subnet membership (operator-ratified 2026-06-13): a subnet\n/// member is already trusted, so a member forging the Gateway type is out of\n/// scope by construction. The proven-node keying is registry-addressing\n/// correctness, not an extra trust layer.\n// [impl->REQ-MSG-6]\nfn origin_user_backed(instances: &[Instance], origin_node: &str) -> bool {\n    instances.iter().any(|i| {\n        i.node == origin_node\n            && i.endpoint_type\n                .as_deref()\n                .is_some_and(|tag| is_gateway_endpoint(&EndpointType::from_tag(tag)))\n    })\n}\n\n/// Wall-clock now (epoch ms) — the reply-window clock.\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n\n/// What the funnel did with one inbound record.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum WanOutcome {\n    /// Delivered live over TCP to the target's listener (op claimed after).\n    DeliveredTcp,\n    /// Delivered by injecting into a broker-held (spt-hosted, relay-less) PTY via\n    /// the live translation binary (op claimed after) — the F-023 WAN-ingress leg.\n    /// Rides the SAME \"delivered\" wire token as [`WanOutcome::DeliveredTcp`] (no\n    /// wire change): both are a confirmed live delivery to the sender.\n    DeliveredInject,\n    /// Spooled for later drain, atomically with the op claim.\n…\n}\n…\n/// the original `msg.from`, never this rendered label.\n// [impl->REQ-MSG-CLI-ORIGIN]\nfn render_delivered_from(from: &str, origin_node: &str) -> String {\n    if from.is_empty() {\n        node_label_display(origin_node, None)\n    } else {\n        from.to_string()\n    }\n}\n\npub fn receive_wan(\n    msg: &WanMessage,\n    origin_node: &str,\n    owlery: &Path,\n    registry: &RegistryHost,\n) -> WanOutcome {\n    // Gate 2 of ADR-0009 (visibility ran at the sender's resolution; grants\n    // gate capabilities, not messages). Messages classify reply-vs-unsolicited\n    // against the target's own recent outbound (the stateful firewall): a\n    // correlated reply passes a whitelist that would refuse the same node\n    // unsolicited.\n    let class = classify_inbound(&msg.target, origin_node, now_ms());\n    if access_check(&msg.target, origin_node, class) == AccessDecision::Deny {\n        return WanOutcome::Refused;\n    }\n\n    if !perch_exists(&msg.target) {\n        return WanOutcome::NoPerch;\n    }\n\n    let perch_path =\n        spt_store::perch::resolve_perch_path(&msg.target, spt_store::perch::ParentHint::Infer);\n\n    // Replay check first: a claimed op is done regardless of which path\n    // claimed it.\n    match spool::wan_seen_at(&perch_path, &msg.op_id) {\n        Ok(true) => return WanOutcome::Duplicate,\n        Ok(false) => {}\n        Err(_) => return WanOutcome::NoPerch,\n    }\n\n    // user-msg identity gate, WAN-ingress half (REQ-MSG-5/6; KH 7.5): re-stamp a\n    // forged/unverified user-msg down to plain msg before it reaches the target.\n    // The origin is provably user-backed iff its advertised endpoint, hosted on\n    // the QUIC-proven origin_node, is Gateway-typed (REQ-MSG-6); absent/unknown\n    // type fail-closes to a re-stamp.\n    let origin_backed = origin_user_backed(&registry.instances_of(&msg.from), origin_node);\n    let delivered_body = restamp_wan_user_msg(&msg.body, origin_backed);\n\n    // CLI-origin render (REQ-MSG-CLI-ORIGIN): an EMPTY `from` (a legacy/foreign\n    // from-less send that the sender-side `cli@<node>` stamp didn't cover) is\n    // rendered as the ORIGIN NODE DISPLAY — `node_label_display(origin_node, None)`\n    // = the QUIC-proven origin node's key-prefix, never blank. The identity gate\n    // above keys on the ORIGINAL `msg.from` (unchanged); only the delivered sender\n    // label is rendered. A non-empty `from` passes through untouched.\n    // [impl->REQ-MSG-CLI-ORIGIN]\n    let delivered_from = render_delivered_from(&msg.from, origin_node);\n\n    // Live-TCP first (mirrors the local TCP-xor-spool discipline, 4.5), claim\n    // after the completed write (see module docs for the window accounting).\n    if deliver_tcp(&msg.target, &delivered_from, &delivered_body, owlery) {\n        let _ = spool::wan_mark_seen_at(&perch_path, &msg.op_id);\n        return WanOutcome::DeliveredTcp;\n    }\n\n    // spt-hosted (broker holds the PTY, no relay): inject into the broker-held PTY\n    // via the live translation binary BEFORE falling to spool (F-023). This closes\n    // the WAN-ingress starvation gap — the leg that existed only in local cmd_send.\n    // Claim discipline mirrors the TCP leg: delivered=true → claim the op + return\n    // the \"delivered\" token (no wire change); anything else (no binary, IDLE-no-\n    // binary, ACTIVE-deferred, broker miss) falls through to the spool-with-claim\n    // transaction below — never a raw PTY write (v0.14.3 LAW, enforced by the\n    // broker's dispatch_endpoint_input).\n    // [impl->REQ-WAN-SPT-HOSTED-DELIVERY]\n    // [impl->REQ-HAZARD-DELIVERY-STARVATION]\n    if let Some((true, _)) =\n        crate::inject::try_spt_hosted_inject(&msg.target, &delivered_from, &delivered_body, owlery, false)\n    {\n        let _ = spool::wan_mark_seen_at(&perch_path, &msg.op_id);\n        return WanOutcome::DeliveredInject;\n    }\n\n    // Offline: claim + spool in one transaction — exactly-once.\n    match spool::spool_wan_message_at(&perch_path, &delivered_from, &delivered_body, &msg.op_id) {\n        Ok(true) => WanOutcome::Spooled,\n        Ok(false) => WanOutcome::Duplicate,\n        Err(_) => WanOutcome::NoPerch,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use spt_proto::event::compose_user_msg_event;\n\n    // [unit->REQ-MSG-CLI-ORIGIN] WAN-ingress render of the delivered sender label:\n    // an EMPTY from renders as the origin node display (key-prefix, never blank); a\n    // non-empty from passes through untouched.\n    #[test]\n    fn wan_ingress_empty_from_renders_origin_node_label() {\n        let origin = \"deadbeefcafef00d\";\n        // Empty from → the origin node's display (node_label_display(origin, None)) —\n        // never blank, and it identifies the QUIC-proven origin node.\n        let rendered = render_delivered_from(\"\", origin);\n        assert!(!rendered.is_empty(), \"an empty from must NEVER render blank\");\n        assert_eq!(rendered, node_label_display(origin, None));\n        // A non-empty from is untouched (the identity gate + reply routing keep it).\n        assert_eq!(render_delivered_from(\"peer-x\", origin), \"peer-x\");\n        assert_eq!(render_delivered_from(\"cli@ENLYZEAM\", origin), \"cli@ENLYZEAM\");\n    }\n\n    // [unit->REQ-WAN-SEND-DELIVERY] the reply-leg token vocabulary round-trips:\n    // every receiver WanOutcome maps to a stable token that the sender decodes\n    // back to the matching request outcome, and an unknown/garbled token is\n    // treated as NoReply (unconfirmed — never optimistically claimed delivered).\n    #[test]\n    fn wan_outcome_token_round_trips_and_unknown_is_noreply() {\n        let pairs = [\n            (WanOutcome::DeliveredTcp, WanRequestOutcome::Delivered),\n            (WanOutcome::DeliveredInject, WanRequestOutcome::Delivered),\n            (WanOutcome::Spooled, WanRequestOutcome::Spooled),\n            (WanOutcome::Duplicate, WanRequestOutcome::Duplicate),\n            (WanOutcome::Refused, WanRequestOutcome::Refused),\n            (WanOutcome::NoPerch, WanRequestOutcome::NoPerch),\n        ];\n        for (outcome, expected) in pairs {\n            assert_eq!(\n                WanRequestOutcome::from_token(outcome.token()),\n                expected,\n                \"{outcome:?} must round-trip through its wire token\"\n            );\n        }\n        // A token from a future/garbled receiver is unconfirmed, never delivered.\n        assert_eq!(\n            WanRequestOutcome::from_token(\"some_future_verb\"),\n            WanRequestOutcome::NoReply\n        );\n        assert_eq!(WanRequestOutcome::from_token(\"\"), WanRequestOutcome::NoReply);\n    }\n\n    // [unit->REQ-MSG-5] WAN-ingress re-stamp (KH 7.5): a user-msg body from an\n    // origin not provably user-backed (the fail-closed default) is re-stamped\n    // down to plain msg, preserving from + the decoded inner body; a user-backed\n    // origin (the Gateway path, REQ-MSG-6) keeps it verbatim; a plain msg\n    // envelope and a bare body are untouched either way.\n    #[test]\n…\n}"}],"details":{"displayContent":{"text":"//! WAN message receive funnel (M4-D5a) — wire record → access gate → op-id\n//! dedup → the existing local delivery path.\n//!\n//! The sending node resolved the target (D3c policy), dialed, and wrote\n//! [`WanMessage`] NDJSON lines onto a broker-owned QUIC stream (D4). This\n//! module is the receiving node's funnel from a decoded record to the local\n//! REQ-MSG machinery — it adds exactly two things the local path doesn't have,\n//! then delegates:\n//!\n//! 1. **the access gate** ([`crate::access`], ADR-0009): every wire-inbound\n//!    record passes [`access_check`] with the **handshake-proven** origin node\n//!    (the broker stream's `remote_id_hex` — never a payload field,\n//!    REQ-HAZARD-WAN-ORIGIN-AUTH);\n//! 2. **op-id idempotency** (REQ-HAZARD-RESTART-IDEMPOTENT): the wire feed\n//!    replays — a sender's journaled redrive, or this brain restarting and\n//!    resubscribing from an earlier cursor — so delivery dedups on the\n//!    sender-minted durable `op_id`.\n//!\n//! ## Exactly-once boundaries (honest accounting)\n//!\n//! - **Spool path (offline target):** fully exactly-once — the op-id claim and\n//!   the spool row commit in ONE SQLite transaction\n//!   ([`spool::spool_wan_message_at`]); a crash on either side of the write\n//!   replays to the same single row.\n//! - **Live-TCP path:** check → TCP write → claim. A crash in the gap between\n//!   a completed TCP write and the claim re-delivers that op on replay (an\n//!   at-least-once window bounded to one in-flight record; the live listener\n//!   path has no durable receipt to dedup against). Claim-before-write was\n//!   rejected because a crash in ITS gap silently drops the message — for a\n//!   messaging substrate, a rare duplicate beats a rare silent drop. Tracked\n//!   in `docs/FAULT-MATRIX.md`.\n\nuse std::borrow::Cow;\nuse std::path::Path;\n\nuse spt_msg::deliver::{deliver_tcp, perch_exists};\nuse spt_net::net::ndjson;\nuse spt_net::net::registry::{node_label_display, Instance};\nuse spt_net::net::wanmsg::{WanMessage, WanReplyDecoder};\n\nuse crate::brain::{Brain, BrokerEvent};\nuse spt_proto::endpoint::EndpointType;\nuse spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};\nuse spt_store::spool;\n\nuse crate::access::{access_check, classify_inbound, AccessDecision};\nuse crate::registryhost::RegistryHost;\n\n/// Re-stamp a WAN-inbound `user-msg` body down to plain `msg` **unless** the\n/// transport-proven origin is a user-backed endpoint (CONTEXT §Gateway; the\n/// KH 7.5 cross-node enforcement — authority is the proven origin, never the\n/// payload). The honored `user-msg` form is a verbatim typed-envelope body, so\n/// a forged one would otherwise reach the receiver as user-authority; here we\n/// decode it and recompose a plain `msg` envelope, preserving `from` + the inner\n/// body. A non-`user-msg` body (a plain message, any other typed envelope, or a\n/// bare body) is returned unchanged.\n///\n/// `origin_user_backed` is resolved by the caller from the proven origin node\n/// ([`origin_user_backed`]): over WAN, the ONLY provably user-backed origin is a\n/// registry-resolved Gateway endpoint advertised on the proven node (REQ-MSG-6).\n/// An origin that advertises no type, or a non-Gateway type, yields `false` and\n/// the `user-msg` is re-stamped — fail-closed by construction.\n// [impl->REQ-MSG-5]\n// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]\nfn restamp_wan_user_msg(body: &str, origin_user_backed: bool) -> Cow<'_, str> {\n    if origin_user_backed {\n        return Cow::Borrowed(body);\n    }\n    match parse_event(body) {\n        Some(ev) if ev.event_type.as_deref() == Some(EVENT_TYPE_USER_MSG) => {\n            Cow::Owned(compose_msg_event(ev.from().unwrap_or(\"\"), &ev.body))\n        }\n        _ => Cow::Borrowed(body),\n    }\n}\n\n/// Resolve whether a WAN-inbound `user-msg`'s origin is provably **user-backed**\n/// (REQ-MSG-6 — CONTEXT §Gateway): among the origin endpoint's advertised\n/// instances, the one hosted on the **QUIC-handshake-proven** `origin_node`\n/// advertises the Gateway type ([`spt_proto::event::GATEWAY_TAG`]). Keyed on the\n/// proven node, NEVER the wire `from` — a record whose `from` claims a gateway\n/// id is inert unless the proven node actually hosts that gateway-typed\n/// endpoint. An absent or non-gateway advertised type → `false`: the fail-closed\n/// re-stamp (an N-1 node that does not yet advertise `endpoint_type` degrades\n/// gracefully to plain `msg`).\n///\n/// This resolves the *advertised* type; it is **not** a forgery defense. The\n/// trust boundary is subnet membership (operator-ratified 2026-06-13): a subnet\n/// member is already trusted, so a member forging the Gateway type is out of\n/// scope by construction. The proven-node keying is registry-addressing\n/// correctness, not an extra trust layer.\n// [impl->REQ-MSG-6]\nfn origin_user_backed(instances: &[Instance], origin_node: &str) -> bool {\n    instances.iter().any(|i| {\n        i.node == origin_node\n            && i.endpoint_type\n                .as_deref()\n                .is_some_and(|tag| is_gateway_endpoint(&EndpointType::from_tag(tag)))\n    })\n}\n\n/// Wall-clock now (epoch ms) — the reply-window clock.\nfn now_ms() -> u64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .map(|d| d.as_millis() as u64)\n        .unwrap_or(0)\n}\n\n/// What the funnel did with one inbound record.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum WanOutcome {\n    /// Delivered live over TCP to the target's listener (op claimed after).\n    DeliveredTcp,\n    /// Delivered by injecting into a broker-held (spt-hosted, relay-less) PTY via\n    /// the live translation binary (op claimed after) — the F-023 WAN-ingress leg.\n    /// Rides the SAME \"delivered\" wire token as [`WanOutcome::DeliveredTcp`] (no\n    /// wire change): both are a confirmed live delivery to the sender.\n    DeliveredInject,\n    /// Spooled for later drain, atomically with the op claim.\n…\n}\n…\n/// the original `msg.from`, never this rendered label.\n// [impl->REQ-MSG-CLI-ORIGIN]\nfn render_delivered_from(from: &str, origin_node: &str) -> String {\n    if from.is_empty() {\n        node_label_display(origin_node, None)\n    } else {\n        from.to_string()\n    }\n}\n\npub fn receive_wan(\n    msg: &WanMessage,\n    origin_node: &str,\n    owlery: &Path,\n    registry: &RegistryHost,\n) -> WanOutcome {\n    // Gate 2 of ADR-0009 (visibility ran at the sender's resolution; grants\n    // gate capabilities, not messages). Messages classify reply-vs-unsolicited\n    // against the target's own recent outbound (the stateful firewall): a\n    // correlated reply passes a whitelist that would refuse the same node\n    // unsolicited.\n    let class = classify_inbound(&msg.target, origin_node, now_ms());\n    if access_check(&msg.target, origin_node, class) == AccessDecision::Deny {\n        return WanOutcome::Refused;\n    }\n\n    if !perch_exists(&msg.target) {\n        return WanOutcome::NoPerch;\n    }\n\n    let perch_path =\n        spt_store::perch::resolve_perch_path(&msg.target, spt_store::perch::ParentHint::Infer);\n\n    // Replay check first: a claimed op is done regardless of which path\n    // claimed it.\n    match spool::wan_seen_at(&perch_path, &msg.op_id) {\n        Ok(true) => return WanOutcome::Duplicate,\n        Ok(false) => {}\n        Err(_) => return WanOutcome::NoPerch,\n    }\n\n    // user-msg identity gate, WAN-ingress half (REQ-MSG-5/6; KH 7.5): re-stamp a\n    // forged/unverified user-msg down to plain msg before it reaches the target.\n    // The origin is provably user-backed iff its advertised endpoint, hosted on\n    // the QUIC-proven origin_node, is Gateway-typed (REQ-MSG-6); absent/unknown\n    // type fail-closes to a re-stamp.\n    let origin_backed = origin_user_backed(&registry.instances_of(&msg.from), origin_node);\n    let delivered_body = restamp_wan_user_msg(&msg.body, origin_backed);\n\n    // CLI-origin render (REQ-MSG-CLI-ORIGIN): an EMPTY `from` (a legacy/foreign\n    // from-less send that the sender-side `cli@<node>` stamp didn't cover) is\n    // rendered as the ORIGIN NODE DISPLAY — `node_label_display(origin_node, None)`\n    // = the QUIC-proven origin node's key-prefix, never blank. The identity gate\n    // above keys on the ORIGINAL `msg.from` (unchanged); only the delivered sender\n    // label is rendered. A non-empty `from` passes through untouched.\n    // [impl->REQ-MSG-CLI-ORIGIN]\n    let delivered_from = render_delivered_from(&msg.from, origin_node);\n\n    // Live-TCP first (mirrors the local TCP-xor-spool discipline, 4.5), claim\n    // after the completed write (see module docs for the window accounting).\n    if deliver_tcp(&msg.target, &delivered_from, &delivered_body, owlery) {\n        let _ = spool::wan_mark_seen_at(&perch_path, &msg.op_id);\n        return WanOutcome::DeliveredTcp;\n    }\n\n    // spt-hosted (broker holds the PTY, no relay): inject into the broker-held PTY\n    // via the live translation binary BEFORE falling to spool (F-023). This closes\n    // the WAN-ingress starvation gap — the leg that existed only in local cmd_send.\n    // Claim discipline mirrors the TCP leg: delivered=true → claim the op + return\n    // the \"delivered\" token (no wire change); anything else (no binary, IDLE-no-\n    // binary, ACTIVE-deferred, broker miss) falls through to the spool-with-claim\n    // transaction below — never a raw PTY write (v0.14.3 LAW, enforced by the\n    // broker's dispatch_endpoint_input).\n    // [impl->REQ-WAN-SPT-HOSTED-DELIVERY]\n    // [impl->REQ-HAZARD-DELIVERY-STARVATION]\n    if let Some((true, _)) =\n        crate::inject::try_spt_hosted_inject(&msg.target, &delivered_from, &delivered_body, owlery, false)\n    {\n        let _ = spool::wan_mark_seen_at(&perch_path, &msg.op_id);\n        return WanOutcome::DeliveredInject;\n    }\n\n    // Offline: claim + spool in one transaction — exactly-once.\n    match spool::spool_wan_message_at(&perch_path, &delivered_from, &delivered_body, &msg.op_id) {\n        Ok(true) => WanOutcome::Spooled,\n        Ok(false) => WanOutcome::Duplicate,\n        Err(_) => WanOutcome::NoPerch,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use spt_proto::event::compose_user_msg_event;\n\n    // [unit->REQ-MSG-CLI-ORIGIN] WAN-ingress render of the delivered sender label:\n    // an EMPTY from renders as the origin node display (key-prefix, never blank); a\n    // non-empty from passes through untouched.\n    #[test]\n    fn wan_ingress_empty_from_renders_origin_node_label() {\n        let origin = \"deadbeefcafef00d\";\n        // Empty from → the origin node's display (node_label_display(origin, None)) —\n        // never blank, and it identifies the QUIC-proven origin node.\n        let rendered = render_delivered_from(\"\", origin);\n        assert!(!rendered.is_empty(), \"an empty from must NEVER render blank\");\n        assert_eq!(rendered, node_label_display(origin, None));\n        // A non-empty from is untouched (the identity gate + reply routing keep it).\n        assert_eq!(render_delivered_from(\"peer-x\", origin), \"peer-x\");\n        assert_eq!(render_delivered_from(\"cli@ENLYZEAM\", origin), \"cli@ENLYZEAM\");\n    }\n\n    // [unit->REQ-WAN-SEND-DELIVERY] the reply-leg token vocabulary round-trips:\n    // every receiver WanOutcome maps to a stable token that the sender decodes\n    // back to the matching request outcome, and an unknown/garbled token is\n    // treated as NoReply (unconfirmed — never optimistically claimed delivered).\n    #[test]\n    fn wan_outcome_token_round_trips_and_unknown_is_noreply() {\n        let pairs = [\n            (WanOutcome::DeliveredTcp, WanRequestOutcome::Delivered),\n            (WanOutcome::DeliveredInject, WanRequestOutcome::Delivered),\n            (WanOutcome::Spooled, WanRequestOutcome::Spooled),\n            (WanOutcome::Duplicate, WanRequestOutcome::Duplicate),\n            (WanOutcome::Refused, WanRequestOutcome::Refused),\n            (WanOutcome::NoPerch, WanRequestOutcome::NoPerch),\n        ];\n        for (outcome, expected) in pairs {\n            assert_eq!(\n                WanRequestOutcome::from_token(outcome.token()),\n                expected,\n                \"{outcome:?} must round-trip through its wire token\"\n            );\n        }\n        // A token from a future/garbled receiver is unconfirmed, never delivered.\n        assert_eq!(\n            WanRequestOutcome::from_token(\"some_future_verb\"),\n            WanRequestOutcome::NoReply\n        );\n        assert_eq!(WanRequestOutcome::from_token(\"\"), WanRequestOutcome::NoReply);\n    }\n\n    // [unit->REQ-MSG-5] WAN-ingress re-stamp (KH 7.5): a user-msg body from an\n    // origin not provably user-backed (the fail-closed default) is re-stamped\n    // down to plain msg, preserving from + the decoded inner body; a user-backed\n    // origin (the Gateway path, REQ-MSG-6) keeps it verbatim; a plain msg\n    // envelope and a bare body are untouched either way.\n    #[test]\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,null,129,null,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,null,530]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\wan.rs"}}},"isError":false,"timestamp":1784197477884}}
{"type":"message","id":"32328bdf","parentId":"a77e0d84","timestamp":"2026-07-16T10:24:37.895Z","message":{"role":"toolResult","toolCallId":"call_yeZ6sFzrw2Rrbeot3TeXpsHv|fc_017bff223290b7ba016a58b165cb14819aa4af32a4bbbca276","toolName":"read","content":[{"type":"text","text":"//! Per-perch SQLite spool for offline message queuing.\n//!\n//! Schema and drain semantics copied verbatim from the sister project\n//! (`claude_skill_owl` `src/common/spool.rs`) per ADR-0001 — `spool.db` is a\n//! \"perch property\": one database at `<perch_path>/spool.db`. This module ports\n//! the **path-aware (`_at`) surface**: every call takes the resolved perch path\n//! directly. The legacy id-based wrappers depend on the perch-path resolver\n//! (T10) and arrive with it; nothing here needs id→path resolution.\n//!\n//! ## Deferred rows — two coupled invariants\n//! A row carries a `deferred` flag. The flag splits two delivery paths:\n//! - **REQ-HAZARD-DEFERRED-DRAIN (1.4):** the event-stream drain\n//!   ([`drain_non_deferred_at`]) MUST exclude deferred rows — they must not\n//!   wake a poll listener / surface on the Monitor stream.\n//! - **REQ-HAZARD-DEFERRED-SURVIVE-DRAIN (4.4):** a deferred row MUST survive\n//!   an event-stream drain undelivered, so the hook channel can still deliver\n//!   it later. [`drain_all_at`] (the hook path) remains unfiltered.\n//!\n//! A drained row is marked `delivered = 1`, never deleted (audit + idempotency).\n\nuse rusqlite::{params, Connection};\nuse std::collections::HashSet;\nuse std::path::{Path, PathBuf};\nuse std::sync::{Mutex, OnceLock};\n\n/// Default message TTL: 0 = infinite (no expiry). Persistent perches retain\n/// messages indefinitely.\nconst DEFAULT_TTL_SECONDS: i64 = 0;\n\n/// Delivery-window axis values (ADR-0028 W2). `default` = eligible for both the\n/// active (hook-poll) and idle (relay/wake) windows; `idle_only` = idle/wake\n/// window only (suppressed from the active hook); `active_only` = active hook\n/// window only, never wakes (the renamed `--deferred`).\npub const WINDOW_DEFAULT: &str = \"default\";\npub const WINDOW_IDLE_ONLY: &str = \"idle_only\";\npub const WINDOW_ACTIVE_ONLY: &str = \"active_only\";\n/// Channel-restriction axis default (W3 wires `prefer_native`/`force_native`).\npub const CHANNEL_ANY: &str = \"any\";\n\n/// The SINGLE window→`deferred` sync point (ADR-0028 W2). The `deferred` column\n/// is RETAINED as a back-compat MIRROR of the window axis — true IFF the window\n/// is `active_only` (hook-poll-only, no wake) — so the ~40 existing\n/// `deferred`-keyed reads stay correct at zero churn. EVERY insert derives the\n/// column through this one fn so the mirror can never silently drift (a unit test\n/// pins the invariant across all three window values).\nfn deferred_for(window: &str) -> i64 {\n    (window == WINDOW_ACTIVE_ONLY) as i64\n}\n\n/// The window-aware INSERT chokepoint — every spool insert funnels through here\n/// so `deferred` is derived from `window` in exactly one place. `created_at` /\n/// TTL are stamped by the caller's `now_epoch()`.\nfn insert_message(\n    conn: &Connection,\n    from_id: &str,\n    body: &str,\n    window: &str,\n    channel: &str,\n    ephemeral: bool,\n) -> rusqlite::Result<()> {\n    conn.execute(\n        \"INSERT INTO messages \\\n         (from_id, body, created_at, ttl_seconds, deferred, \\\"window\\\", channel, ephemeral) \\\n         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)\",\n        params![\n            from_id,\n            body,\n            now_epoch(),\n            DEFAULT_TTL_SECONDS,\n            deferred_for(window),\n            window,\n            channel,\n            ephemeral as i64\n        ],\n    )?;\n    Ok(())\n}\n\n/// Current epoch seconds.\nfn now_epoch() -> i64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .expect(\"system clock before UNIX epoch\")\n        .as_secs() as i64\n}\n\n/// Open (creating if needed) the spool database at `<perch_path>/spool.db`,\n/// creating the perch directory if absent. WAL + busy_timeout for concurrent\n/// readers/writers. The `ALTER TABLE` migrations are idempotent — a second\n/// open silently no-ops them (SQLite errors on duplicate column; caught).\n// [impl->REQ-HAZARD-DEFERRED-DRAIN]\npub fn open_spool_at(perch_path: &Path) -> rusqlite::Result<Connection> {\n    if !perch_path.exists() {\n        std::fs::create_dir_all(perch_path)\n            .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;\n    }\n    let db_path = perch_path.join(\"spool.db\");\n    let conn = Connection::open(&db_path)?;\n    // [impl->REQ-HAZARD-REGISTRY-CONCURRENT] concurrency-safe pragmas — see\n    // crate::db::tune_connection (busy_timeout first, WAL-with-retry).\n    crate::db::tune_connection(&conn)?;\n    conn.execute_batch(\n        \"CREATE TABLE IF NOT EXISTS messages (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            from_id TEXT NOT NULL,\n            body TEXT NOT NULL,\n            created_at INTEGER NOT NULL,\n            ttl_seconds INTEGER NOT NULL DEFAULT 0,\n            delivered INTEGER NOT NULL DEFAULT 0,\n            deferred INTEGER NOT NULL DEFAULT 0\n        )\",\n    )?;\n    // Idempotent migrations for pre-existing databases (duplicate-column error\n    // is expected and swallowed).\n    let _ =\n        conn.execute_batch(\"ALTER TABLE messages ADD COLUMN delivered INTEGER NOT NULL DEFAULT 0\");\n    let _ =\n        conn.execute_batch(\"ALTER TABLE messages ADD COLUMN deferred INTEGER NOT NULL DEFAULT 0\");\n    // v0.15.0 W2 (ADR-0028) per-message send-modifier axes — additive columns,\n    // same idempotent-ALTER pattern as `deferred`. `window` is the delivery-window\n    // axis (`default` | `idle_only` | `active_only`); `channel` is the native-channel\n    // restriction (`any` | `prefer_native` | `force_native`, wired in W3); `ephemeral`\n    // (bool) is the persistence axis (drop if undeliverable in the accepted window).\n    // The `deferred` column is retained and kept in sync (deferred=1 IFF\n    // window='active_only') so the existing deferred-keyed reads are unaffected.\n    // `window` is a SQLite keyword (window functions) — always double-quoted in SQL.\n    let _ = conn\n        .execute_batch(\"ALTER TABLE messages ADD COLUMN \\\"window\\\" TEXT NOT NULL DEFAULT 'default'\");\n    let _ =\n        conn.execute_batch(\"ALTER TABLE messages ADD COLUMN channel TEXT NOT NULL DEFAULT 'any'\");\n    let _ = conn\n        .execute_batch(\"ALTER TABLE messages ADD COLUMN ephemeral INTEGER NOT NULL DEFAULT 0\");\n    // W5 (REQ-SPOOL-TAKE-AUDIT) taker-audit columns — additive + NULLABLE (no schema\n    // break; delivered rows are already retained, never deleted). Stamped in the SAME\n    // UPDATE that flips `delivered = 1`, so a taken row records WHO took it: the leg\n    // (`relay-backlog`|`hook-poll`|`idle-inject`|`psyche`), the taker's sid + pid, and\n    // the take wall-ms. NULL on an untaken row OR a legacy/unaudited take (degrade-safe).\n    let _ = conn.execute_batch(\"ALTER TABLE messages ADD COLUMN taken_leg TEXT\");\n    let _ = conn.execute_batch(\"ALTER TABLE messages ADD COLUMN taken_sid TEXT\");\n    let _ = conn.execute_batch(\"ALTER TABLE messages ADD COLUMN taken_pid INTEGER\");\n    let _ = conn.execute_batch(\"ALTER TABLE messages ADD COLUMN taken_at_ms INTEGER\");\n    // WAN-inbound idempotency keys (M4-D5a): op_ids this perch has already\n    // accepted, so a replayed wire feed can't double-deliver. Same database as\n    // the messages table so the seen-mark and the spool row commit in ONE\n    // transaction (REQ-HAZARD-RESTART-IDEMPOTENT at the spool boundary).\n    conn.execute_batch(\n        \"CREATE TABLE IF NOT EXISTS wan_seen (\n            op_id TEXT PRIMARY KEY,\n            at_ms INTEGER NOT NULL\n        )\",\n    )?;\n…\n}\n…\n\n/// The `--json` debug read (W5, REQ-SPOOL-TAKE-AUDIT): every retained row with its taker\n/// provenance, oldest-first. Non-destructive — reads the audit columns, marks nothing.\npub fn audit_rows_at(perch_path: &Path) -> rusqlite::Result<Vec<SpoolAuditRow>> {\n    let conn = open_spool_at(perch_path)?;\n    let mut stmt = conn.prepare(\n        \"SELECT id, from_id, delivered, taken_leg, taken_sid, taken_pid, taken_at_ms \\\n         FROM messages ORDER BY id ASC\",\n    )?;\n    let rows = stmt\n        .query_map([], |row| {\n            Ok(SpoolAuditRow {\n                id: row.get::<_, i64>(0)?,\n                from: row.get::<_, String>(1)?,\n                delivered: row.get::<_, i64>(2)? != 0,\n                taken_leg: row.get::<_, Option<String>>(3)?,\n                taken_sid: row.get::<_, Option<String>>(4)?,\n                taken_pid: row.get::<_, Option<i64>>(5)?.map(|v| v as u32),\n                taken_at_ms: row.get::<_, Option<i64>>(6)?.map(|v| v as u64),\n            })\n        })?\n        .filter_map(|r| r.ok())\n        .collect();\n    Ok(rows)\n}\n\n/// Insert a normal (non-deferred) message and touch `.has-messages`.\n// [impl->REQ-HAZARD-DEFERRED-DRAIN]\npub fn spool_message_at(perch_path: &Path, from_id: &str, body: &str) -> rusqlite::Result<()> {\n    let conn = open_spool_at(perch_path)?;\n    insert_message(&conn, from_id, body, WINDOW_DEFAULT, CHANNEL_ANY, false)?;\n    touch_has_messages(perch_path);\n    Ok(())\n}\n\n/// Insert a message carrying explicit send-modifier axes (ADR-0028 W2):\n/// `window` (`default`|`idle_only`|`active_only`), `channel` (`any` for W2;\n/// `prefer_native`/`force_native` wire in W3), and `ephemeral`. `deferred` is\n/// derived from `window` at the chokepoint. Touches `.has-messages`.\n// [impl->REQ-MSG-DELIVERY-AXES]\npub fn spool_message_windowed_at(\n    perch_path: &Path,\n    from_id: &str,\n    body: &str,\n    window: &str,\n    channel: &str,\n    ephemeral: bool,\n) -> rusqlite::Result<()> {\n    let conn = open_spool_at(perch_path)?;\n    insert_message(&conn, from_id, body, window, channel, ephemeral)?;\n    touch_has_messages(perch_path);\n    Ok(())\n}\n\n/// Insert a deferred message (hook-channel only; excluded from the event-stream\n/// drain) and touch `.has-messages`.\n// [impl->REQ-HAZARD-DEFERRED-SURVIVE-DRAIN]\npub fn spool_message_deferred_at(\n    perch_path: &Path,\n    from_id: &str,\n    body: &str,\n) -> rusqlite::Result<()> {\n    let conn = open_spool_at(perch_path)?;\n    insert_message(&conn, from_id, body, WINDOW_ACTIVE_ONLY, CHANNEL_ANY, false)?;\n    touch_has_messages(perch_path);\n    Ok(())\n}\n\n/// Insert a WAN-inbound message **iff its `op_id` is new**, atomically: the\n/// `wan_seen` claim and the spool row commit in one transaction, so a crash\n/// can never leave the op claimed-but-unspooled (drop) or spooled-but-unclaimed\n/// (dup on replay). Returns whether the row was inserted (`false` = duplicate\n/// replay, nothing written). This is the wire feed's exactly-once point for\n/// offline targets (REQ-HAZARD-RESTART-IDEMPOTENT, the spool boundary).\n// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n// [impl->REQ-NET-1]\npub fn spool_wan_message_at(\n    perch_path: &Path,\n    from_id: &str,\n    body: &str,\n    op_id: &str,\n) -> rusqlite::Result<bool> {\n    let now_ms = std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .expect(\"system clock before UNIX epoch\")\n        .as_millis() as i64;\n    let conn = open_spool_at(perch_path)?;\n    conn.execute_batch(\"BEGIN IMMEDIATE\")?;\n    let claimed = conn.execute(\n        \"INSERT OR IGNORE INTO wan_seen (op_id, at_ms) VALUES (?1, ?2)\",\n        params![op_id, now_ms],\n    )?;\n    if claimed == 0 {\n        // Already seen — a replay. Nothing to write.\n        conn.execute_batch(\"COMMIT\")?;\n        return Ok(false);\n    }\n    insert_message(&conn, from_id, body, WINDOW_DEFAULT, CHANNEL_ANY, false)?;\n    conn.execute_batch(\"COMMIT\")?;\n    touch_has_messages(perch_path);\n    Ok(true)\n}\n\n/// True if this perch has already accepted WAN op `op_id`.\npub fn wan_seen_at(perch_path: &Path, op_id: &str) -> rusqlite::Result<bool> {\n    let conn = open_spool_at(perch_path)?;\n    let n: i64 = conn.query_row(\n        \"SELECT COUNT(*) FROM wan_seen WHERE op_id = ?1\",\n        params![op_id],\n        |r| r.get(0),\n    )?;\n    Ok(n > 0)\n}\n\n/// Mark WAN op `op_id` seen without writing a spool row (the live-TCP delivery\n/// path marks after a successful live handoff). Returns whether the mark was\n/// new (`false` = already seen).\n// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\npub fn wan_mark_seen_at(perch_path: &Path, op_id: &str) -> rusqlite::Result<bool> {\n    let now_ms = std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .expect(\"system clock before UNIX epoch\")\n        .as_millis() as i64;\n    let conn = open_spool_at(perch_path)?;\n    let n = conn.execute(\n        \"INSERT OR IGNORE INTO wan_seen (op_id, at_ms) VALUES (?1, ?2)\",\n        params![op_id, now_ms],\n    )?;\n    Ok(n > 0)\n}\n\n/// Purge expired rows (finite TTL only).\nfn purge_expired(conn: &Connection) -> rusqlite::Result<()> {\n    let now = now_epoch();\n    conn.execute(\n        \"DELETE FROM messages WHERE ttl_seconds > 0 AND created_at + ttl_seconds < ?1\",\n        params![now],\n    )?;\n    Ok(())\n}\n\n/// Drain (mark delivered + return) every undelivered row, ordered by id.\n/// **Unfiltered** — includes deferred rows (the hook delivery path). This is\n/// what lets a deferred row reach the user even though the event stream skips\n/// it. Unaudited (legacy/test callers); see [`drain_all_audited_at`] for the\n/// taker-audit-stamping variant.\n// [impl->REQ-HAZARD-DEFERRED-SURVIVE-DRAIN]\npub fn drain_all_at(perch_path: &Path) -> rusqlite::Result<Vec<DrainedMsg>> {\n    drain_where(perch_path, false, None)\n}\n\n…\npub fn release_at(perch_path: &Path, msg_ids: &[i64]) -> rusqlite::Result<()> {\n    if msg_ids.is_empty() {\n…\n    }\n    let conn = open_spool_at(perch_path)?;\n    conn.execute_batch(\"BEGIN IMMEDIATE\")?;\n    for id in msg_ids {\n        conn.execute(\n            \"UPDATE messages SET delivered = 0, taken_leg = NULL, taken_sid = NULL, \\\n             taken_pid = NULL, taken_at_ms = NULL WHERE id = ?1\",\n            params![id],\n        )?;\n    }\n    conn.execute_batch(\"COMMIT\")?;\n    Ok(())\n}\n\n/// Drain the single oldest undelivered row (any deferred status), or `None`. Unaudited;\n/// see [`drain_one_audited_at`].\npub fn drain_one_at(perch_path: &Path) -> rusqlite::Result<Option<DrainedMsg>> {\n    drain_one_inner(perch_path, None)\n}\n\n/// [`drain_one_at`] stamping the taker audit (W5, REQ-SPOOL-TAKE-AUDIT).\n// [impl->REQ-SPOOL-TAKE-AUDIT]\npub fn drain_one_audited_at(\n    perch_path: &Path,\n    audit: &TakerAudit,\n) -> rusqlite::Result<Option<DrainedMsg>> {\n    drain_one_inner(perch_path, Some(audit))\n}\n\n/// Rides the shared claim kernel too — a single-row take is still a take.\n/// [impl->REQ-CARRIER-CLAIM-EXCLUSIVE]\nfn drain_one_inner(\n    perch_path: &Path,\n    audit: Option<&TakerAudit>,\n) -> rusqlite::Result<Option<DrainedMsg>> {\n    let conn = open_spool_at(perch_path)?;\n    purge_expired(&conn)?;\n    Ok(claim_rows(\n        &conn,\n        \"SELECT id, from_id, body FROM messages WHERE delivered = 0 ORDER BY id ASC LIMIT 1\",\n        audit,\n    )?\n    .into_iter()\n    .next()\n    .map(|(_, from, body)| DrainedMsg { from, body }))\n}\n\n/// Non-destructive read of all undelivered rows: `(id, from_id, body, created_at)`.\npub fn peek_all_at(perch_path: &Path) -> rusqlite::Result<Vec<(i64, String, String, i64)>> {\n    let conn = open_spool_at(perch_path)?;\n    purge_expired(&conn)?;\n    let mut stmt = conn.prepare(\n        \"SELECT id, from_id, body, created_at FROM messages WHERE delivered = 0 ORDER BY id ASC\",\n    )?;\n    let rows = stmt\n        .query_map([], |row| {\n            Ok((\n                row.get::<_, i64>(0)?,\n                row.get::<_, String>(1)?,\n                row.get::<_, String>(2)?,\n                row.get::<_, i64>(3)?,\n            ))\n        })?\n        .filter_map(|r| r.ok())\n        .collect();\n    Ok(rows)\n}\n\n/// Non-destructive read of undelivered NON-DEFERRED rows: `(id, from_id, body)`.\npub fn peek_non_deferred_at(perch_path: &Path) -> rusqlite::Result<Vec<(i64, String, String)>> {\n    let conn = open_spool_at(perch_path)?;\n    purge_expired(&conn)?;\n    let mut stmt = conn.prepare(\n        \"SELECT id, from_id, body FROM messages WHERE delivered = 0 AND deferred = 0 ORDER BY id ASC\",\n    )?;\n    let rows = stmt\n        .query_map([], |row| {\n            Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?))\n        })?\n        .filter_map(|r| r.ok())\n        .collect();\n    Ok(rows)\n}\n\n/// Count of undelivered, non-expired rows (all deferred statuses).\npub fn pending_count_at(perch_path: &Path) -> rusqlite::Result<usize> {\n    let conn = open_spool_at(perch_path)?;\n    let now = now_epoch();\n    let n: i64 = conn.query_row(\n        \"SELECT COUNT(*) FROM messages WHERE delivered = 0 AND (ttl_seconds <= 0 OR created_at + ttl_seconds >= ?1)\",\n        params![now],\n        |r| r.get(0),\n    )?;\n    Ok(n as usize)\n}\n\n/// Count of undelivered, non-expired, **non-deferred** rows (event-stream view).\n// [impl->REQ-HAZARD-DEFERRED-DRAIN]\npub fn pending_count_non_deferred_at(perch_path: &Path) -> rusqlite::Result<usize> {\n    let conn = open_spool_at(perch_path)?;\n    let now = now_epoch();\n    let n: i64 = conn.query_row(\n        \"SELECT COUNT(*) FROM messages WHERE delivered = 0 AND deferred = 0 AND (ttl_seconds <= 0 OR created_at + ttl_seconds >= ?1)\",\n        params![now],\n        |r| r.get(0),\n    )?;\n    Ok(n as usize)\n}\n\n/// Ephemeral evaporation (ADR-0028 W3): DELETE undelivered, non-deferred,\n/// `ephemeral` rows — the persistence-axis drop \"at window-open with no live\n/// carrier\". The caller invokes this at the idle-transition window-open once it\n/// has determined no working binary carrier took the rows; durable (non-ephemeral)\n/// rows are left spooled for the relay. Returns the number evaporated.\n///\n/// NOTE (v0.15.0 partial, doyle-ratified): this covers the spt-hosted-binary\n/// no-carrier leg + TTL (via `purge_expired`); the harness-hosted relay\n/// \"no live listener\" leg is NOT yet delivered (it needs relay carrier-presence\n/// detection, deferred — see CONTEXT.md / REQ-MSG-DELIVERY-AXES).\n// [impl->REQ-MSG-DELIVERY-AXES]\npub fn evaporate_ephemeral_non_deferred_at(perch_path: &Path) -> rusqlite::Result<usize> {\n    let conn = open_spool_at(perch_path)?;\n    let n = conn.execute(\n        \"DELETE FROM messages WHERE delivered = 0 AND deferred = 0 AND ephemeral = 1\",\n        [],\n    )?;\n    Ok(n)\n}\n\n/// Mark a set of message ids delivered, in one transaction. Empty input is a\n/// no-op.\npub fn mark_delivered_at(perch_path: &Path, msg_ids: &[i64]) -> rusqlite::Result<()> {\n    if msg_ids.is_empty() {\n        return Ok(());\n    }\n    let conn = open_spool_at(perch_path)?;\n    conn.execute_batch(\"BEGIN IMMEDIATE\")?;\n    for id in msg_ids {\n        conn.execute(\n            \"UPDATE messages SET delivered = 1 WHERE id = ?1\",\n            params![id],\n        )?;\n    }\n    conn.execute_batch(\"COMMIT\")?;\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn perch() -> tempfile::TempDir {\n        tempfile::tempdir().unwrap()\n    }\n\n    // [unit->REQ-SPOOL-TAKE-AUDIT] an audited take stamps WHO took the row (leg + sid +\n    // pid + at_ms) in the same UPDATE as delivered=1, and audit_rows_at surfaces it; an\n    // UNAUDITED take flips delivered but leaves the taker columns NULL (degrade-safe).\n    #[test]\n    fn audited_take_records_taker_provenance_unaudited_leaves_null() {\n        let d = perch();\n        let p = d.path();\n        spool_message_at(p, \"alice\", \"audited\").unwrap();\n        spool_message_at(p, \"bob\", \"unaudited\").unwrap();\n\n        // Audited drain of the first row (relay-backlog leg with a sid + pid).\n        let audit = TakerAudit::new(TakerLeg::RelayBacklog, Some(\"sess-xyz\".to_string()), Some(4242));\n        let got = drain_non_deferred_audited_at(p, &audit).unwrap();\n        assert_eq!(got.len(), 2, \"both undelivered rows drain\");\n\n        // Both are now delivered; re-inspect provenance via the --json read.\n        let rows = audit_rows_at(p).unwrap();\n        assert_eq!(rows.len(), 2, \"retained rows (taken, never deleted)\");\n        assert!(rows.iter().all(|r| r.delivered), \"both marked delivered\");\n        // Every row taken by THIS audited drain carries the leg + sid + pid.\n        for r in &rows {\n            assert_eq!(r.taken_leg.as_deref(), Some(\"relay-backlog\"));\n            assert_eq!(r.taken_sid.as_deref(), Some(\"sess-xyz\"));\n            assert_eq!(r.taken_pid, Some(4242));\n            assert!(r.taken_at_ms.is_some(), \"take wall-ms stamped\");\n        }\n\n        // A second perch drained UNAUDITED leaves the taker columns NULL.\n        let d2 = perch();\n        let p2 = d2.path();\n        spool_message_at(p2, \"carol\", \"legacy\").unwrap();\n        let _ = drain_non_deferred_at(p2).unwrap();\n        let rows2 = audit_rows_at(p2).unwrap();\n        assert_eq!(rows2.len(), 1);\n        assert!(rows2[0].delivered, \"delivered flips\");\n        assert_eq!(rows2[0].taken_leg, None, \"unaudited take stamps no provenance\");\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"//! Per-perch SQLite spool for offline message queuing.\n//!\n//! Schema and drain semantics copied verbatim from the sister project\n//! (`claude_skill_owl` `src/common/spool.rs`) per ADR-0001 — `spool.db` is a\n//! \"perch property\": one database at `<perch_path>/spool.db`. This module ports\n//! the **path-aware (`_at`) surface**: every call takes the resolved perch path\n//! directly. The legacy id-based wrappers depend on the perch-path resolver\n//! (T10) and arrive with it; nothing here needs id→path resolution.\n//!\n//! ## Deferred rows — two coupled invariants\n//! A row carries a `deferred` flag. The flag splits two delivery paths:\n//! - **REQ-HAZARD-DEFERRED-DRAIN (1.4):** the event-stream drain\n//!   ([`drain_non_deferred_at`]) MUST exclude deferred rows — they must not\n//!   wake a poll listener / surface on the Monitor stream.\n//! - **REQ-HAZARD-DEFERRED-SURVIVE-DRAIN (4.4):** a deferred row MUST survive\n//!   an event-stream drain undelivered, so the hook channel can still deliver\n//!   it later. [`drain_all_at`] (the hook path) remains unfiltered.\n//!\n//! A drained row is marked `delivered = 1`, never deleted (audit + idempotency).\n\nuse rusqlite::{params, Connection};\nuse std::collections::HashSet;\nuse std::path::{Path, PathBuf};\nuse std::sync::{Mutex, OnceLock};\n\n/// Default message TTL: 0 = infinite (no expiry). Persistent perches retain\n/// messages indefinitely.\nconst DEFAULT_TTL_SECONDS: i64 = 0;\n\n/// Delivery-window axis values (ADR-0028 W2). `default` = eligible for both the\n/// active (hook-poll) and idle (relay/wake) windows; `idle_only` = idle/wake\n/// window only (suppressed from the active hook); `active_only` = active hook\n/// window only, never wakes (the renamed `--deferred`).\npub const WINDOW_DEFAULT: &str = \"default\";\npub const WINDOW_IDLE_ONLY: &str = \"idle_only\";\npub const WINDOW_ACTIVE_ONLY: &str = \"active_only\";\n/// Channel-restriction axis default (W3 wires `prefer_native`/`force_native`).\npub const CHANNEL_ANY: &str = \"any\";\n\n/// The SINGLE window→`deferred` sync point (ADR-0028 W2). The `deferred` column\n/// is RETAINED as a back-compat MIRROR of the window axis — true IFF the window\n/// is `active_only` (hook-poll-only, no wake) — so the ~40 existing\n/// `deferred`-keyed reads stay correct at zero churn. EVERY insert derives the\n/// column through this one fn so the mirror can never silently drift (a unit test\n/// pins the invariant across all three window values).\nfn deferred_for(window: &str) -> i64 {\n    (window == WINDOW_ACTIVE_ONLY) as i64\n}\n\n/// The window-aware INSERT chokepoint — every spool insert funnels through here\n/// so `deferred` is derived from `window` in exactly one place. `created_at` /\n/// TTL are stamped by the caller's `now_epoch()`.\nfn insert_message(\n    conn: &Connection,\n    from_id: &str,\n    body: &str,\n    window: &str,\n    channel: &str,\n    ephemeral: bool,\n) -> rusqlite::Result<()> {\n    conn.execute(\n        \"INSERT INTO messages \\\n         (from_id, body, created_at, ttl_seconds, deferred, \\\"window\\\", channel, ephemeral) \\\n         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)\",\n        params![\n            from_id,\n            body,\n            now_epoch(),\n            DEFAULT_TTL_SECONDS,\n            deferred_for(window),\n            window,\n            channel,\n            ephemeral as i64\n        ],\n    )?;\n    Ok(())\n}\n\n/// Current epoch seconds.\nfn now_epoch() -> i64 {\n    std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .expect(\"system clock before UNIX epoch\")\n        .as_secs() as i64\n}\n\n/// Open (creating if needed) the spool database at `<perch_path>/spool.db`,\n/// creating the perch directory if absent. WAL + busy_timeout for concurrent\n/// readers/writers. The `ALTER TABLE` migrations are idempotent — a second\n/// open silently no-ops them (SQLite errors on duplicate column; caught).\n// [impl->REQ-HAZARD-DEFERRED-DRAIN]\npub fn open_spool_at(perch_path: &Path) -> rusqlite::Result<Connection> {\n    if !perch_path.exists() {\n        std::fs::create_dir_all(perch_path)\n            .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;\n    }\n    let db_path = perch_path.join(\"spool.db\");\n    let conn = Connection::open(&db_path)?;\n    // [impl->REQ-HAZARD-REGISTRY-CONCURRENT] concurrency-safe pragmas — see\n    // crate::db::tune_connection (busy_timeout first, WAL-with-retry).\n    crate::db::tune_connection(&conn)?;\n    conn.execute_batch(\n        \"CREATE TABLE IF NOT EXISTS messages (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            from_id TEXT NOT NULL,\n            body TEXT NOT NULL,\n            created_at INTEGER NOT NULL,\n            ttl_seconds INTEGER NOT NULL DEFAULT 0,\n            delivered INTEGER NOT NULL DEFAULT 0,\n            deferred INTEGER NOT NULL DEFAULT 0\n        )\",\n    )?;\n    // Idempotent migrations for pre-existing databases (duplicate-column error\n    // is expected and swallowed).\n    let _ =\n        conn.execute_batch(\"ALTER TABLE messages ADD COLUMN delivered INTEGER NOT NULL DEFAULT 0\");\n    let _ =\n        conn.execute_batch(\"ALTER TABLE messages ADD COLUMN deferred INTEGER NOT NULL DEFAULT 0\");\n    // v0.15.0 W2 (ADR-0028) per-message send-modifier axes — additive columns,\n    // same idempotent-ALTER pattern as `deferred`. `window` is the delivery-window\n    // axis (`default` | `idle_only` | `active_only`); `channel` is the native-channel\n    // restriction (`any` | `prefer_native` | `force_native`, wired in W3); `ephemeral`\n    // (bool) is the persistence axis (drop if undeliverable in the accepted window).\n    // The `deferred` column is retained and kept in sync (deferred=1 IFF\n    // window='active_only') so the existing deferred-keyed reads are unaffected.\n    // `window` is a SQLite keyword (window functions) — always double-quoted in SQL.\n    let _ = conn\n        .execute_batch(\"ALTER TABLE messages ADD COLUMN \\\"window\\\" TEXT NOT NULL DEFAULT 'default'\");\n    let _ =\n        conn.execute_batch(\"ALTER TABLE messages ADD COLUMN channel TEXT NOT NULL DEFAULT 'any'\");\n    let _ = conn\n        .execute_batch(\"ALTER TABLE messages ADD COLUMN ephemeral INTEGER NOT NULL DEFAULT 0\");\n    // W5 (REQ-SPOOL-TAKE-AUDIT) taker-audit columns — additive + NULLABLE (no schema\n    // break; delivered rows are already retained, never deleted). Stamped in the SAME\n    // UPDATE that flips `delivered = 1`, so a taken row records WHO took it: the leg\n    // (`relay-backlog`|`hook-poll`|`idle-inject`|`psyche`), the taker's sid + pid, and\n    // the take wall-ms. NULL on an untaken row OR a legacy/unaudited take (degrade-safe).\n    let _ = conn.execute_batch(\"ALTER TABLE messages ADD COLUMN taken_leg TEXT\");\n    let _ = conn.execute_batch(\"ALTER TABLE messages ADD COLUMN taken_sid TEXT\");\n    let _ = conn.execute_batch(\"ALTER TABLE messages ADD COLUMN taken_pid INTEGER\");\n    let _ = conn.execute_batch(\"ALTER TABLE messages ADD COLUMN taken_at_ms INTEGER\");\n    // WAN-inbound idempotency keys (M4-D5a): op_ids this perch has already\n    // accepted, so a replayed wire feed can't double-deliver. Same database as\n    // the messages table so the seen-mark and the spool row commit in ONE\n    // transaction (REQ-HAZARD-RESTART-IDEMPOTENT at the spool boundary).\n    conn.execute_batch(\n        \"CREATE TABLE IF NOT EXISTS wan_seen (\n            op_id TEXT PRIMARY KEY,\n            at_ms INTEGER NOT NULL\n        )\",\n    )?;\n…\n}\n…\n\n/// The `--json` debug read (W5, REQ-SPOOL-TAKE-AUDIT): every retained row with its taker\n/// provenance, oldest-first. Non-destructive — reads the audit columns, marks nothing.\npub fn audit_rows_at(perch_path: &Path) -> rusqlite::Result<Vec<SpoolAuditRow>> {\n    let conn = open_spool_at(perch_path)?;\n    let mut stmt = conn.prepare(\n        \"SELECT id, from_id, delivered, taken_leg, taken_sid, taken_pid, taken_at_ms \\\n         FROM messages ORDER BY id ASC\",\n    )?;\n    let rows = stmt\n        .query_map([], |row| {\n            Ok(SpoolAuditRow {\n                id: row.get::<_, i64>(0)?,\n                from: row.get::<_, String>(1)?,\n                delivered: row.get::<_, i64>(2)? != 0,\n                taken_leg: row.get::<_, Option<String>>(3)?,\n                taken_sid: row.get::<_, Option<String>>(4)?,\n                taken_pid: row.get::<_, Option<i64>>(5)?.map(|v| v as u32),\n                taken_at_ms: row.get::<_, Option<i64>>(6)?.map(|v| v as u64),\n            })\n        })?\n        .filter_map(|r| r.ok())\n        .collect();\n    Ok(rows)\n}\n\n/// Insert a normal (non-deferred) message and touch `.has-messages`.\n// [impl->REQ-HAZARD-DEFERRED-DRAIN]\npub fn spool_message_at(perch_path: &Path, from_id: &str, body: &str) -> rusqlite::Result<()> {\n    let conn = open_spool_at(perch_path)?;\n    insert_message(&conn, from_id, body, WINDOW_DEFAULT, CHANNEL_ANY, false)?;\n    touch_has_messages(perch_path);\n    Ok(())\n}\n\n/// Insert a message carrying explicit send-modifier axes (ADR-0028 W2):\n/// `window` (`default`|`idle_only`|`active_only`), `channel` (`any` for W2;\n/// `prefer_native`/`force_native` wire in W3), and `ephemeral`. `deferred` is\n/// derived from `window` at the chokepoint. Touches `.has-messages`.\n// [impl->REQ-MSG-DELIVERY-AXES]\npub fn spool_message_windowed_at(\n    perch_path: &Path,\n    from_id: &str,\n    body: &str,\n    window: &str,\n    channel: &str,\n    ephemeral: bool,\n) -> rusqlite::Result<()> {\n    let conn = open_spool_at(perch_path)?;\n    insert_message(&conn, from_id, body, window, channel, ephemeral)?;\n    touch_has_messages(perch_path);\n    Ok(())\n}\n\n/// Insert a deferred message (hook-channel only; excluded from the event-stream\n/// drain) and touch `.has-messages`.\n// [impl->REQ-HAZARD-DEFERRED-SURVIVE-DRAIN]\npub fn spool_message_deferred_at(\n    perch_path: &Path,\n    from_id: &str,\n    body: &str,\n) -> rusqlite::Result<()> {\n    let conn = open_spool_at(perch_path)?;\n    insert_message(&conn, from_id, body, WINDOW_ACTIVE_ONLY, CHANNEL_ANY, false)?;\n    touch_has_messages(perch_path);\n    Ok(())\n}\n\n/// Insert a WAN-inbound message **iff its `op_id` is new**, atomically: the\n/// `wan_seen` claim and the spool row commit in one transaction, so a crash\n/// can never leave the op claimed-but-unspooled (drop) or spooled-but-unclaimed\n/// (dup on replay). Returns whether the row was inserted (`false` = duplicate\n/// replay, nothing written). This is the wire feed's exactly-once point for\n/// offline targets (REQ-HAZARD-RESTART-IDEMPOTENT, the spool boundary).\n// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\n// [impl->REQ-NET-1]\npub fn spool_wan_message_at(\n    perch_path: &Path,\n    from_id: &str,\n    body: &str,\n    op_id: &str,\n) -> rusqlite::Result<bool> {\n    let now_ms = std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .expect(\"system clock before UNIX epoch\")\n        .as_millis() as i64;\n    let conn = open_spool_at(perch_path)?;\n    conn.execute_batch(\"BEGIN IMMEDIATE\")?;\n    let claimed = conn.execute(\n        \"INSERT OR IGNORE INTO wan_seen (op_id, at_ms) VALUES (?1, ?2)\",\n        params![op_id, now_ms],\n    )?;\n    if claimed == 0 {\n        // Already seen — a replay. Nothing to write.\n        conn.execute_batch(\"COMMIT\")?;\n        return Ok(false);\n    }\n    insert_message(&conn, from_id, body, WINDOW_DEFAULT, CHANNEL_ANY, false)?;\n    conn.execute_batch(\"COMMIT\")?;\n    touch_has_messages(perch_path);\n    Ok(true)\n}\n\n/// True if this perch has already accepted WAN op `op_id`.\npub fn wan_seen_at(perch_path: &Path, op_id: &str) -> rusqlite::Result<bool> {\n    let conn = open_spool_at(perch_path)?;\n    let n: i64 = conn.query_row(\n        \"SELECT COUNT(*) FROM wan_seen WHERE op_id = ?1\",\n        params![op_id],\n        |r| r.get(0),\n    )?;\n    Ok(n > 0)\n}\n\n/// Mark WAN op `op_id` seen without writing a spool row (the live-TCP delivery\n/// path marks after a successful live handoff). Returns whether the mark was\n/// new (`false` = already seen).\n// [impl->REQ-HAZARD-RESTART-IDEMPOTENT]\npub fn wan_mark_seen_at(perch_path: &Path, op_id: &str) -> rusqlite::Result<bool> {\n    let now_ms = std::time::SystemTime::now()\n        .duration_since(std::time::UNIX_EPOCH)\n        .expect(\"system clock before UNIX epoch\")\n        .as_millis() as i64;\n    let conn = open_spool_at(perch_path)?;\n    let n = conn.execute(\n        \"INSERT OR IGNORE INTO wan_seen (op_id, at_ms) VALUES (?1, ?2)\",\n        params![op_id, now_ms],\n    )?;\n    Ok(n > 0)\n}\n\n/// Purge expired rows (finite TTL only).\nfn purge_expired(conn: &Connection) -> rusqlite::Result<()> {\n    let now = now_epoch();\n    conn.execute(\n        \"DELETE FROM messages WHERE ttl_seconds > 0 AND created_at + ttl_seconds < ?1\",\n        params![now],\n    )?;\n    Ok(())\n}\n\n/// Drain (mark delivered + return) every undelivered row, ordered by id.\n/// **Unfiltered** — includes deferred rows (the hook delivery path). This is\n/// what lets a deferred row reach the user even though the event stream skips\n/// it. Unaudited (legacy/test callers); see [`drain_all_audited_at`] for the\n/// taker-audit-stamping variant.\n// [impl->REQ-HAZARD-DEFERRED-SURVIVE-DRAIN]\npub fn drain_all_at(perch_path: &Path) -> rusqlite::Result<Vec<DrainedMsg>> {\n    drain_where(perch_path, false, None)\n}\n\n…\npub fn release_at(perch_path: &Path, msg_ids: &[i64]) -> rusqlite::Result<()> {\n    if msg_ids.is_empty() {\n…\n    }\n    let conn = open_spool_at(perch_path)?;\n    conn.execute_batch(\"BEGIN IMMEDIATE\")?;\n    for id in msg_ids {\n        conn.execute(\n            \"UPDATE messages SET delivered = 0, taken_leg = NULL, taken_sid = NULL, \\\n             taken_pid = NULL, taken_at_ms = NULL WHERE id = ?1\",\n            params![id],\n        )?;\n    }\n    conn.execute_batch(\"COMMIT\")?;\n    Ok(())\n}\n\n/// Drain the single oldest undelivered row (any deferred status), or `None`. Unaudited;\n/// see [`drain_one_audited_at`].\npub fn drain_one_at(perch_path: &Path) -> rusqlite::Result<Option<DrainedMsg>> {\n    drain_one_inner(perch_path, None)\n}\n\n/// [`drain_one_at`] stamping the taker audit (W5, REQ-SPOOL-TAKE-AUDIT).\n// [impl->REQ-SPOOL-TAKE-AUDIT]\npub fn drain_one_audited_at(\n    perch_path: &Path,\n    audit: &TakerAudit,\n) -> rusqlite::Result<Option<DrainedMsg>> {\n    drain_one_inner(perch_path, Some(audit))\n}\n\n/// Rides the shared claim kernel too — a single-row take is still a take.\n/// [impl->REQ-CARRIER-CLAIM-EXCLUSIVE]\nfn drain_one_inner(\n    perch_path: &Path,\n    audit: Option<&TakerAudit>,\n) -> rusqlite::Result<Option<DrainedMsg>> {\n    let conn = open_spool_at(perch_path)?;\n    purge_expired(&conn)?;\n    Ok(claim_rows(\n        &conn,\n        \"SELECT id, from_id, body FROM messages WHERE delivered = 0 ORDER BY id ASC LIMIT 1\",\n        audit,\n    )?\n    .into_iter()\n    .next()\n    .map(|(_, from, body)| DrainedMsg { from, body }))\n}\n\n/// Non-destructive read of all undelivered rows: `(id, from_id, body, created_at)`.\npub fn peek_all_at(perch_path: &Path) -> rusqlite::Result<Vec<(i64, String, String, i64)>> {\n    let conn = open_spool_at(perch_path)?;\n    purge_expired(&conn)?;\n    let mut stmt = conn.prepare(\n        \"SELECT id, from_id, body, created_at FROM messages WHERE delivered = 0 ORDER BY id ASC\",\n    )?;\n    let rows = stmt\n        .query_map([], |row| {\n            Ok((\n                row.get::<_, i64>(0)?,\n                row.get::<_, String>(1)?,\n                row.get::<_, String>(2)?,\n                row.get::<_, i64>(3)?,\n            ))\n        })?\n        .filter_map(|r| r.ok())\n        .collect();\n    Ok(rows)\n}\n\n/// Non-destructive read of undelivered NON-DEFERRED rows: `(id, from_id, body)`.\npub fn peek_non_deferred_at(perch_path: &Path) -> rusqlite::Result<Vec<(i64, String, String)>> {\n    let conn = open_spool_at(perch_path)?;\n    purge_expired(&conn)?;\n    let mut stmt = conn.prepare(\n        \"SELECT id, from_id, body FROM messages WHERE delivered = 0 AND deferred = 0 ORDER BY id ASC\",\n    )?;\n    let rows = stmt\n        .query_map([], |row| {\n            Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?))\n        })?\n        .filter_map(|r| r.ok())\n        .collect();\n    Ok(rows)\n}\n\n/// Count of undelivered, non-expired rows (all deferred statuses).\npub fn pending_count_at(perch_path: &Path) -> rusqlite::Result<usize> {\n    let conn = open_spool_at(perch_path)?;\n    let now = now_epoch();\n    let n: i64 = conn.query_row(\n        \"SELECT COUNT(*) FROM messages WHERE delivered = 0 AND (ttl_seconds <= 0 OR created_at + ttl_seconds >= ?1)\",\n        params![now],\n        |r| r.get(0),\n    )?;\n    Ok(n as usize)\n}\n\n/// Count of undelivered, non-expired, **non-deferred** rows (event-stream view).\n// [impl->REQ-HAZARD-DEFERRED-DRAIN]\npub fn pending_count_non_deferred_at(perch_path: &Path) -> rusqlite::Result<usize> {\n    let conn = open_spool_at(perch_path)?;\n    let now = now_epoch();\n    let n: i64 = conn.query_row(\n        \"SELECT COUNT(*) FROM messages WHERE delivered = 0 AND deferred = 0 AND (ttl_seconds <= 0 OR created_at + ttl_seconds >= ?1)\",\n        params![now],\n        |r| r.get(0),\n    )?;\n    Ok(n as usize)\n}\n\n/// Ephemeral evaporation (ADR-0028 W3): DELETE undelivered, non-deferred,\n/// `ephemeral` rows — the persistence-axis drop \"at window-open with no live\n/// carrier\". The caller invokes this at the idle-transition window-open once it\n/// has determined no working binary carrier took the rows; durable (non-ephemeral)\n/// rows are left spooled for the relay. Returns the number evaporated.\n///\n/// NOTE (v0.15.0 partial, doyle-ratified): this covers the spt-hosted-binary\n/// no-carrier leg + TTL (via `purge_expired`); the harness-hosted relay\n/// \"no live listener\" leg is NOT yet delivered (it needs relay carrier-presence\n/// detection, deferred — see CONTEXT.md / REQ-MSG-DELIVERY-AXES).\n// [impl->REQ-MSG-DELIVERY-AXES]\npub fn evaporate_ephemeral_non_deferred_at(perch_path: &Path) -> rusqlite::Result<usize> {\n    let conn = open_spool_at(perch_path)?;\n    let n = conn.execute(\n        \"DELETE FROM messages WHERE delivered = 0 AND deferred = 0 AND ephemeral = 1\",\n        [],\n    )?;\n    Ok(n)\n}\n\n/// Mark a set of message ids delivered, in one transaction. Empty input is a\n/// no-op.\npub fn mark_delivered_at(perch_path: &Path, msg_ids: &[i64]) -> rusqlite::Result<()> {\n    if msg_ids.is_empty() {\n        return Ok(());\n    }\n    let conn = open_spool_at(perch_path)?;\n    conn.execute_batch(\"BEGIN IMMEDIATE\")?;\n    for id in msg_ids {\n        conn.execute(\n            \"UPDATE messages SET delivered = 1 WHERE id = ?1\",\n            params![id],\n        )?;\n    }\n    conn.execute_batch(\"COMMIT\")?;\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn perch() -> tempfile::TempDir {\n        tempfile::tempdir().unwrap()\n    }\n\n    // [unit->REQ-SPOOL-TAKE-AUDIT] an audited take stamps WHO took the row (leg + sid +\n    // pid + at_ms) in the same UPDATE as delivered=1, and audit_rows_at surfaces it; an\n    // UNAUDITED take flips delivered but leaves the taker columns NULL (degrade-safe).\n    #[test]\n    fn audited_take_records_taker_provenance_unaudited_leaves_null() {\n        let d = perch();\n        let p = d.path();\n        spool_message_at(p, \"alice\", \"audited\").unwrap();\n        spool_message_at(p, \"bob\", \"unaudited\").unwrap();\n\n        // Audited drain of the first row (relay-backlog leg with a sid + pid).\n        let audit = TakerAudit::new(TakerLeg::RelayBacklog, Some(\"sess-xyz\".to_string()), Some(4242));\n        let got = drain_non_deferred_audited_at(p, &audit).unwrap();\n        assert_eq!(got.len(), 2, \"both undelivered rows drain\");\n\n        // Both are now delivered; re-inspect provenance via the --json read.\n        let rows = audit_rows_at(p).unwrap();\n        assert_eq!(rows.len(), 2, \"retained rows (taken, never deleted)\");\n        assert!(rows.iter().all(|r| r.delivered), \"both marked delivered\");\n        // Every row taken by THIS audited drain carries the leg + sid + pid.\n        for r in &rows {\n            assert_eq!(r.taken_leg.as_deref(), Some(\"relay-backlog\"));\n            assert_eq!(r.taken_sid.as_deref(), Some(\"sess-xyz\"));\n            assert_eq!(r.taken_pid, Some(4242));\n            assert!(r.taken_at_ms.is_some(), \"take wall-ms stamped\");\n        }\n\n        // A second perch drained UNAUDITED leaves the taker columns NULL.\n        let d2 = perch();\n        let p2 = d2.path();\n        spool_message_at(p2, \"carol\", \"legacy\").unwrap();\n        let _ = drain_non_deferred_at(p2).unwrap();\n        let rows2 = audit_rows_at(p2).unwrap();\n        assert_eq!(rows2.len(), 1);\n        assert!(rows2[0].delivered, \"delivered flips\");\n        assert_eq!(rows2[0].taken_leg, None, \"unaudited take stamps no provenance\");\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,null,153,null,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,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,null,677,678,null,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,null,873,null,1527]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\spool.rs"}}},"isError":false,"timestamp":1784197477894}}
{"type":"message","id":"d72c0ad7","parentId":"32328bdf","timestamp":"2026-07-16T10:24:37.897Z","message":{"role":"toolResult","toolCallId":"call_xBzEnykSRMm7zzntTR5BWg2x|fc_017bff223290b7ba016a58b165cb10819ab9638cce7aacc17f","toolName":"read","content":[{"type":"text","text":"//! The thin, stateless in-session relay (B3) — the only residue of the interim\n//! `api listen` runner that stays in the harness-hosted session (topology 1).\n//!\n//! Consolidation (REQ-DAEMON-1) moves the *lifecycle* — Psyche spawn, pulse,\n//! echo, drop-ingest — into the daemon brain as scheduled in-process loops. What\n//! remains running inside the agent's own session is just this relay: it forwards\n//! inbound messages (the offline spool backlog + live poll deliveries) to a sink\n//! (stdout for the real session) and holds **no state of its own**.\n//!\n//! Because it is stateless and every undelivered message is durable in the perch\n//! spool, the relay is **freely killable and respawnable without losing state**:\n//! a killed relay leaves its backlog in the spool, and a fresh relay re-drains it\n//! on its next pump. That is the property the daemon needs — a routine that kills\n//! the in-session relay (e.g. to swap it) never drops a message.\n// [impl->REQ-DAEMON-1]\n\nuse std::path::{Path, PathBuf};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::time::Duration;\n\nuse spt_msg::listener::PollListener;\nuse spt_store::spool;\n\n/// Poll window for the relay's blocking accept — bounds each cycle so the loop\n/// stays responsive to a stop signal (mirrors the interim listener cadence).\npub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);\n\n/// A stateless message relay for one perch: a poll listener bound to the id plus\n/// the perch path it drains. It carries no message buffer — every byte it\n/// forwards comes straight from the live poll or the durable spool.\npub struct Relay {\n    id: String,\n    perch_path: PathBuf,\n    listener: PollListener,\n}\n\nimpl Relay {\n    /// Bind a relay for `id` draining `perch_path`, with the default poll window.\n    pub fn bind(id: &str, perch_path: &Path) -> std::io::Result<Self> {\n        Self::bind_with(id, perch_path, DEFAULT_RELAY_POLL)\n    }\n\n    /// Bind a relay with an explicit poll window.\n    pub fn bind_with(id: &str, perch_path: &Path, poll: Duration) -> std::io::Result<Self> {\n        let listener = PollListener::bind_blocking(id, poll)\n            .map_err(|e| std::io::Error::other(e.to_string()))?;\n        Ok(Relay {\n            id: id.to_string(),\n            perch_path: perch_path.to_path_buf(),\n            listener,\n        })\n    }\n\n    /// The relayed endpoint id.\n    pub fn id(&self) -> &str {\n        &self.id\n    }\n\n    /// Drain the durable offline backlog (non-deferred spool rows) to `sink` as\n    /// structural `(from, body)` (the sink composes the `<EVENT>` at its edge,\n    /// ADR-0020). Idempotent across respawns: a fresh relay re-drains whatever a\n    /// killed one left behind. Returns the number of messages forwarded.\n    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {\n        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg — stamp who drained the row.\n        let audit =\n            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\n        let msgs = spool::drain_non_deferred_audited_at(&self.perch_path, &audit).unwrap_or_default();\n        let n = msgs.len();\n        for m in &msgs {\n            sink(&m.from, &m.body);\n        }\n        n\n    }\n\n    /// One relay cycle: forward a single live poll delivery (if one arrives in the\n    /// poll window) then drain the offline backlog. Holds no state between calls.\n    pub fn pump_once<F: FnMut(&str, &str)>(&self, mut sink: F) {\n        if let Some((from, body)) = self.listener.check_message_blocking() {\n            sink(&from, &body);\n        }\n        self.drain_backlog(&mut sink);\n    }\n\n    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as\n    /// `(from, body)`. The blocking poll bounds each iteration, so a kill signal\n    /// is honored within one poll window. State loss is impossible: the loop\n    /// buffers nothing.\n    pub fn run<F: FnMut(&str, &str)>(&self, stop: &AtomicBool, mut sink: F) {\n        // Drain whatever a predecessor relay left behind before streaming live.\n        self.drain_backlog(&mut sink);\n        while !stop.load(Ordering::Relaxed) {\n            self.pump_once(&mut sink);\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::test_home::with_home;\n    use spt_store::perch::{resolve_perch_path, ParentHint};\n\n    fn perch_for(id: &str) -> PathBuf {\n        let p = resolve_perch_path(id, ParentHint::Infer);\n        std::fs::create_dir_all(&p).unwrap();\n        p\n    }\n\n    #[test] // [unit->REQ-DAEMON-1] a respawned relay re-drains a killed relay's backlog — no loss.\n    fn respawn_loses_no_spooled_message() {\n        with_home(|_| {\n            let perch = perch_for(\"doyle\");\n            spool::spool_message_at(&perch, \"alice\", \"m1\").unwrap();\n…\n        });\n    }\n…\n}\n\n[Showing lines 1-113 of 182. Use :114 to continue]"}],"details":{"truncation":{"content":"//! The thin, stateless in-session relay (B3) — the only residue of the interim\r\n//! `api listen` runner that stays in the harness-hosted session (topology 1).\r\n//!\r\n//! Consolidation (REQ-DAEMON-1) moves the *lifecycle* — Psyche spawn, pulse,\r\n//! echo, drop-ingest — into the daemon brain as scheduled in-process loops. What\r\n//! remains running inside the agent's own session is just this relay: it forwards\r\n//! inbound messages (the offline spool backlog + live poll deliveries) to a sink\r\n//! (stdout for the real session) and holds **no state of its own**.\r\n//!\r\n//! Because it is stateless and every undelivered message is durable in the perch\r\n//! spool, the relay is **freely killable and respawnable without losing state**:\r\n//! a killed relay leaves its backlog in the spool, and a fresh relay re-drains it\r\n//! on its next pump. That is the property the daemon needs — a routine that kills\r\n//! the in-session relay (e.g. to swap it) never drops a message.\r\n// [impl->REQ-DAEMON-1]\r\n\r\nuse std::path::{Path, PathBuf};\r\nuse std::sync::atomic::{AtomicBool, Ordering};\r\nuse std::time::Duration;\r\n\r\nuse spt_msg::listener::PollListener;\r\nuse spt_store::spool;\r\n\r\n/// Poll window for the relay's blocking accept — bounds each cycle so the loop\r\n/// stays responsive to a stop signal (mirrors the interim listener cadence).\r\npub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);\r\n\r\n/// A stateless message relay for one perch: a poll listener bound to the id plus\r\n/// the perch path it drains. It carries no message buffer — every byte it\r\n/// forwards comes straight from the live poll or the durable spool.\r\npub struct Relay {\r\n    id: String,\r\n    perch_path: PathBuf,\r\n    listener: PollListener,\r\n}\r\n\r\nimpl Relay {\r\n    /// Bind a relay for `id` draining `perch_path`, with the default poll window.\r\n    pub fn bind(id: &str, perch_path: &Path) -> std::io::Result<Self> {\r\n        Self::bind_with(id, perch_path, DEFAULT_RELAY_POLL)\r\n    }\r\n\r\n    /// Bind a relay with an explicit poll window.\r\n    pub fn bind_with(id: &str, perch_path: &Path, poll: Duration) -> std::io::Result<Self> {\r\n        let listener = PollListener::bind_blocking(id, poll)\r\n            .map_err(|e| std::io::Error::other(e.to_string()))?;\r\n        Ok(Relay {\r\n            id: id.to_string(),\r\n            perch_path: perch_path.to_path_buf(),\r\n            listener,\r\n        })\r\n    }\r\n\r\n    /// The relayed endpoint id.\r\n    pub fn id(&self) -> &str {\r\n        &self.id\r\n    }\r\n\r\n    /// Drain the durable offline backlog (non-deferred spool rows) to `sink` as\r\n    /// structural `(from, body)` (the sink composes the `<EVENT>` at its edge,\r\n    /// ADR-0020). Idempotent across respawns: a fresh relay re-drains whatever a\r\n    /// killed one left behind. Returns the number of messages forwarded.\r\n    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {\r\n        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg — stamp who drained the row.\r\n        let audit =\r\n            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\r\n        let msgs = spool::drain_non_deferred_audited_at(&self.perch_path, &audit).unwrap_or_default();\r\n        let n = msgs.len();\r\n        for m in &msgs {\r\n            sink(&m.from, &m.body);\r\n        }\r\n        n\r\n    }\r\n\r\n    /// One relay cycle: forward a single live poll delivery (if one arrives in the\r\n    /// poll window) then drain the offline backlog. Holds no state between calls.\r\n    pub fn pump_once<F: FnMut(&str, &str)>(&self, mut sink: F) {\r\n        if let Some((from, body)) = self.listener.check_message_blocking() {\r\n            sink(&from, &body);\r\n        }\r\n        self.drain_backlog(&mut sink);\r\n    }\r\n\r\n    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as\r\n    /// `(from, body)`. The blocking poll bounds each iteration, so a kill signal\r\n    /// is honored within one poll window. State loss is impossible: the loop\r\n    /// buffers nothing.\r\n    pub fn run<F: FnMut(&str, &str)>(&self, stop: &AtomicBool, mut sink: F) {\r\n        // Drain whatever a predecessor relay left behind before streaming live.\r\n        self.drain_backlog(&mut sink);\r\n        while !stop.load(Ordering::Relaxed) {\r\n            self.pump_once(&mut sink);\r\n        }\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::test_home::with_home;\r\n    use spt_store::perch::{resolve_perch_path, ParentHint};\r\n\r\n    fn perch_for(id: &str) -> PathBuf {\r\n        let p = resolve_perch_path(id, ParentHint::Infer);\r\n        std::fs::create_dir_all(&p).unwrap();\r\n        p\r\n    }\r\n\r\n    #[test] // [unit->REQ-DAEMON-1] a respawned relay re-drains a killed relay's backlog — no loss.\r\n    fn respawn_loses_no_spooled_message() {\r\n        with_home(|_| {\r\n            let perch = perch_for(\"doyle\");\r\n            spool::spool_message_at(&perch, \"alice\", \"m1\").unwrap();\r","truncated":true,"truncatedBy":"lines","totalLines":182,"totalBytes":4924,"outputLines":113,"outputBytes":4924,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! The thin, stateless in-session relay (B3) — the only residue of the interim\r\n//! `api listen` runner that stays in the harness-hosted session (topology 1).\r\n//!\r\n//! Consolidation (REQ-DAEMON-1) moves the *lifecycle* — Psyche spawn, pulse,\r\n//! echo, drop-ingest — into the daemon brain as scheduled in-process loops. What\r\n//! remains running inside the agent's own session is just this relay: it forwards\r\n//! inbound messages (the offline spool backlog + live poll deliveries) to a sink\r\n//! (stdout for the real session) and holds **no state of its own**.\r\n//!\r\n//! Because it is stateless and every undelivered message is durable in the perch\r\n//! spool, the relay is **freely killable and respawnable without losing state**:\r\n//! a killed relay leaves its backlog in the spool, and a fresh relay re-drains it\r\n//! on its next pump. That is the property the daemon needs — a routine that kills\r\n//! the in-session relay (e.g. to swap it) never drops a message.\r\n// [impl->REQ-DAEMON-1]\r\n\r\nuse std::path::{Path, PathBuf};\r\nuse std::sync::atomic::{AtomicBool, Ordering};\r\nuse std::time::Duration;\r\n\r\nuse spt_msg::listener::PollListener;\r\nuse spt_store::spool;\r\n\r\n/// Poll window for the relay's blocking accept — bounds each cycle so the loop\r\n/// stays responsive to a stop signal (mirrors the interim listener cadence).\r\npub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);\r\n\r\n/// A stateless message relay for one perch: a poll listener bound to the id plus\r\n/// the perch path it drains. It carries no message buffer — every byte it\r\n/// forwards comes straight from the live poll or the durable spool.\r\npub struct Relay {\r\n    id: String,\r\n    perch_path: PathBuf,\r\n    listener: PollListener,\r\n}\r\n\r\nimpl Relay {\r\n    /// Bind a relay for `id` draining `perch_path`, with the default poll window.\r\n    pub fn bind(id: &str, perch_path: &Path) -> std::io::Result<Self> {\r\n        Self::bind_with(id, perch_path, DEFAULT_RELAY_POLL)\r\n    }\r\n\r\n    /// Bind a relay with an explicit poll window.\r\n    pub fn bind_with(id: &str, perch_path: &Path, poll: Duration) -> std::io::Result<Self> {\r\n        let listener = PollListener::bind_blocking(id, poll)\r\n            .map_err(|e| std::io::Error::other(e.to_string()))?;\r\n        Ok(Relay {\r\n            id: id.to_string(),\r\n            perch_path: perch_path.to_path_buf(),\r\n            listener,\r\n        })\r\n    }\r\n\r\n    /// The relayed endpoint id.\r\n    pub fn id(&self) -> &str {\r\n        &self.id\r\n    }\r\n\r\n    /// Drain the durable offline backlog (non-deferred spool rows) to `sink` as\r\n    /// structural `(from, body)` (the sink composes the `<EVENT>` at its edge,\r\n    /// ADR-0020). Idempotent across respawns: a fresh relay re-drains whatever a\r\n    /// killed one left behind. Returns the number of messages forwarded.\r\n    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {\r\n        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg — stamp who drained the row.\r\n        let audit =\r\n            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\r\n        let msgs = spool::drain_non_deferred_audited_at(&self.perch_path, &audit).unwrap_or_default();\r\n        let n = msgs.len();\r\n        for m in &msgs {\r\n            sink(&m.from, &m.body);\r\n        }\r\n        n\r\n    }\r\n\r\n    /// One relay cycle: forward a single live poll delivery (if one arrives in the\r\n    /// poll window) then drain the offline backlog. Holds no state between calls.\r\n    pub fn pump_once<F: FnMut(&str, &str)>(&self, mut sink: F) {\r\n        if let Some((from, body)) = self.listener.check_message_blocking() {\r\n            sink(&from, &body);\r\n        }\r\n        self.drain_backlog(&mut sink);\r\n    }\r\n\r\n    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as\r\n    /// `(from, body)`. The blocking poll bounds each iteration, so a kill signal\r\n    /// is honored within one poll window. State loss is impossible: the loop\r\n    /// buffers nothing.\r\n    pub fn run<F: FnMut(&str, &str)>(&self, stop: &AtomicBool, mut sink: F) {\r\n        // Drain whatever a predecessor relay left behind before streaming live.\r\n        self.drain_backlog(&mut sink);\r\n        while !stop.load(Ordering::Relaxed) {\r\n            self.pump_once(&mut sink);\r\n        }\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::test_home::with_home;\r\n    use spt_store::perch::{resolve_perch_path, ParentHint};\r\n\r\n    fn perch_for(id: &str) -> PathBuf {\r\n        let p = resolve_perch_path(id, ParentHint::Infer);\r\n        std::fs::create_dir_all(&p).unwrap();\r\n        p\r\n    }\r\n\r\n    #[test] // [unit->REQ-DAEMON-1] a respawned relay re-drains a killed relay's backlog — no loss.\r\n    fn respawn_loses_no_spooled_message() {\r\n        with_home(|_| {\r\n            let perch = perch_for(\"doyle\");\r\n            spool::spool_message_at(&perch, \"alice\", \"m1\").unwrap();\r\n…\n        });\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,null,132,133,null,181]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\relay.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":182,"totalBytes":4924,"outputLines":113,"outputBytes":4924,"shownRange":{"start":1,"end":113},"nextOffset":114}}},"isError":false,"timestamp":1784197477896}}
{"type":"message","id":"ec5bb4df","parentId":"d72c0ad7","timestamp":"2026-07-16T10:24:38.017Z","message":{"role":"toolResult","toolCallId":"call_jYxvgRnoEI92kN9CUhMHqCda|fc_017bff223290b7ba016a58b165cae8819a93e2172a66fadc7b","toolName":"read","content":[{"type":"text","text":"fn apply_user_msg_gate(\n…\n    json: Option<&str>,\n) -> (String, bool) {\n    let empty = body.trim().is_empty();\n    // The user-authority type decision (unchanged security gate): honored only from\n    // a user-backed local origin; an agent-family sender is re-stamped to plain msg.\n    let (event_type, restamped) = if !requested || empty {\n        (EVENT_TYPE_MSG, false)\n    } else if gate_user_msg_type(true, &classify_local_origin(self_id)) == EVENT_TYPE_USER_MSG {\n        (EVENT_TYPE_USER_MSG, false)\n    } else {\n        (EVENT_TYPE_MSG, true) // requested but not user-backed → degraded, loud\n    };\n    // [impl->REQ-MSG-DELIVERY-AXES]\n    match json {\n        // json present (non-empty body) → pre-compose the typed envelope WITH the\n        // json attr so it rides verbatim. Empty body → no message (json is metadata\n        // alongside the body, never a replacement); fall through to the raw path.\n        Some(j) if !empty => (\n            compose_typed_event(event_type, &[(\"from\", from), (\"json\", j)], body),\n            restamped,\n        ),\n        // No json: preserve the prior behavior exactly.\n        _ if event_type == EVENT_TYPE_USER_MSG => (compose_user_msg_event(from, body), false),\n        _ => (body.to_string(), restamped),\n    }\n}\n\n// The spt-hosted delivery leg (predicate `is_spt_hosted_no_relay` + injector\n// `try_spt_hosted_inject`) lives in `spt_daemon::inject` — the ONE shared\n// implementation both this local `spt send` path and the cross-node WAN ingress\n// (`receive_wan`) call, so they cannot drift (F-023, REQ-WAN-SPT-HOSTED-DELIVERY).\n// The CLI keeps its own `spt_daemon::is_running()` fast-path guard before the\n// inject (the daemon may legitimately be down for a CLI invocation); the shared\n// injector itself does not depend on that seed-control ping.\n\n#[allow(clippy::too_many_arguments)]\nfn cmd_send(\n    target: String,\n    from: Option<String>,\n    idle_only: bool,\n    active_only: bool,\n    ephemeral: bool,\n    prefer_native: bool,\n    force_native: bool,\n    json_payload: Option<String>,\n    user_msg: bool,\n) -> i32 {\n    // A bare CLI `spt send` with no owning perch composes an empty `from`; stamp\n    // `cli@<node-label>` so the receiver never sees a blank sender (REQ-MSG-CLI-ORIGIN).\n    // Scoped to `send` (a legal from-less command); `cmd_ring` keeps its NO_SELF refusal.\n    // [impl->REQ-MSG-CLI-ORIGIN]\n    let from = {\n        let f = resolve_from(from);\n        if f.is_empty() {\n            cli_origin_label()\n        } else {\n            f\n        }\n    };\n    // Trim stdin like cmd_notify (REQ-HAZARD-ENVELOPE-CR-LINESAFE parity): a\n    // Windows `echo | spt send` pipes a trailing CRLF — strip it so the message\n    // has no trailing blank line (the codec also neutralizes any interior CR).\n    let body = read_stdin();\n    let body = body.trim();\n    let owlery = perch::owlery_dir();\n\n    // user-msg identity gate (REQ-MSG-5): the requested type is honored only\n    // from a user-backed LOCAL origin — keyed on the *session-proven* self\n    // identity ([`roster::detect_self_id`]), never the `--from` reply label.\n    // An agent-family sender is re-stamped to plain `msg`, loudly. The honored\n    // form is a pre-composed `user-msg` envelope that rides the verbatim\n    // typed-envelope rails (spool/TCP/WAN) and renders `<EVENT type=\"user-msg\">`\n    // at the receiver; the empty-body refusal is preserved (no compose on empty).\n    let (body_to_send, restamped) = apply_user_msg_gate(\n        roster::detect_self_id().as_deref(),\n        &from,\n        body,\n        user_msg,\n        json_payload.as_deref(),\n    );\n    if restamped {\n        eprintln!(\n            \"USER_MSG_RESTAMP:{target}: sender is not a user-backed origin — delivered as plain msg\"\n        );\n    }\n    let body = body_to_send.as_str();\n\n    // FORCE-NATIVE (ADR-0028 W3) is binary-or-nothing: ONLY a broker inject through\n    // the translation binary delivers; any miss is a LOUD not-delivered (honest, no\n    // silent drop), never spooled or relayed (the sender explicitly opted into\n    // binary-or-nothing — no durable persistence). Native bypasses the idle gate.\n    //\n    // F029 C-1 SEAM-2 rider: the UNDELIVERED message is DIFFERENTIATED BY LEG so a\n    // field repro pins the cause from CLI output alone (the pre-C-1 identical message\n    // for every falsy leg made SEAM-2 undiagnosable). `leg=` markers: `empty`\n    // (nothing to send); `cli-gate-no-daemon` (no daemon → no broker to inject\n    // through); `cli-gate-not-hosted` (the target is not a broker-hosted controllable\n    // endpoint — a relay/harness-hosted or non-controllable perch — OR the broker was\n    // unreachable); `broker-spooled-active` (the broker HAS the session but the\n    // endpoint is ACTIVE mid-turn); `broker-spooled-no-binary` (idle but no working\n    // translation binary — faulted/absent, or no hosted session). SEAM-2's own root\n    // (the boundary registration/gate loss) is a separate open item.\n    // [impl->REQ-MSG-DELIVERY-AXES]\n    if force_native {\n        if body.is_empty() {\n            eprintln!(\n                \"FORCE_NATIVE_UNDELIVERED:{target}: empty message (leg=empty) — \\\n                 --force-native is binary-or-nothing (not spooled)\"\n            );\n            return 1;\n        }\n        if !spt_daemon::is_running() {\n            eprintln!(\n                \"FORCE_NATIVE_UNDELIVERED:{target}: daemon not running — no broker to \\\n                 inject through (leg=cli-gate-no-daemon); --force-native is \\\n                 binary-or-nothing (not spooled)\"\n            );\n            return 1;\n        }\n        return match spt_daemon::try_spt_hosted_inject(&target, &from, body, &owlery, true) {\n            Some((true, _)) => {\n                ok_line(format!(\"SENT:{target}\"));\n                0\n            }\n            Some((false, true)) => {\n                eprintln!(\n                    \"FORCE_NATIVE_UNDELIVERED:{target}: broker spooled — endpoint is \\\n                     ACTIVE (mid-turn, no idle inject window) (leg=broker-spooled-active); \\\n                     --force-native is binary-or-nothing (not spooled)\"\n                );\n                1\n            }\n            Some((false, false)) => {\n                eprintln!(\n                    \"FORCE_NATIVE_UNDELIVERED:{target}: broker spooled — no working \\\n                     translation binary (idle but faulted/absent, or no hosted session) \\\n                     (leg=broker-spooled-no-binary); --force-native is binary-or-nothing \\\n                     (not spooled)\"\n                );\n                1\n            }\n            None => {\n                eprintln!(\n                    \"FORCE_NATIVE_UNDELIVERED:{target}: not a broker-hosted controllable \\\n                     endpoint, or the broker was unreachable (leg=cli-gate-not-hosted); \\\n                     --force-native is binary-or-nothing (not spooled)\"\n                );\n                1\n            }\n        };\n    }\n\n    // The sender's delivery WINDOW (ADR-0028 W2): default (both windows) /\n    // idle_only / active_only — a CALLER-side spool tag deciding which drain surface\n    // later picks the row up (active hook poll vs idle/wake relay).\n    let window = if active_only {\n        spt_store::spool::WINDOW_ACTIVE_ONLY\n    } else if idle_only {\n        spt_store::spool::WINDOW_IDLE_ONLY\n    } else {\n        spt_store::spool::WINDOW_DEFAULT\n    };\n\n    // CHANNEL axis (W3) + spt-hosted PTY-inject (REQ-SEND-SPT-HOSTED): an spt-hosted\n    // endpoint (broker holds its PTY) injects the rendered <EVENT> when the broker\n    // delivers. `--prefer-native` injects through the binary REGARDLESS of idle/active\n    // (native bypasses the gate, `native=true`), falling back to the normal\n    // spool-per-window on a miss. A non-native default/idle_only send injects only\n    // when the broker says the target is idle (the activity gate, `native=false`).\n    // A non-native `--active-only` is the explicit hook channel: spool-only, no inject\n    // attempt. Any miss spools tagged with the window (never lost).\n    // [impl->REQ-MSG-DELIVERY-AXES]\n    let try_inject = prefer_native || !active_only;\n    let outcome = if try_inject && !body.is_empty() && spt_daemon::is_running() {\n        match spt_daemon::try_spt_hosted_inject(&target, &from, body, &owlery, prefer_native) {\n            Some((true, _)) => SendOutcome::Sent,\n            Some((false, _)) | None => {\n                deliver::send_windowed(&target, &from, body, &owlery, window, ephemeral)\n            }\n        }\n    } else {\n        deliver::send_windowed(&target, &from, body, &owlery, window, ephemeral)\n    };\n\n    // REQ-IDLE-PARKED-DELIVERY (primary — send-time): a non-active-only send that spooled\n    // to an ALREADY-idle spt-hosted target will never get an ACTIVE→IDLE edge (the F-023\n    // idle-edge drain only fires on the transition), so it would park until an operator\n    // poke. Re-offer the just-spooled row through the inject leg NOW; the pulse-tick belt\n    // is the backstop. On a successful inject the send is honestly SENT, not QUEUED.\n    // [impl->REQ-IDLE-PARKED-DELIVERY]\n…\n}\n\n[Showing lines 6419-6608 of 18900. Use :6609 to continue]"}],"details":{"truncation":{"content":"    json: Option<&str>,\r\n) -> (String, bool) {\r\n    let empty = body.trim().is_empty();\r\n    // The user-authority type decision (unchanged security gate): honored only from\r\n    // a user-backed local origin; an agent-family sender is re-stamped to plain msg.\r\n    let (event_type, restamped) = if !requested || empty {\r\n        (EVENT_TYPE_MSG, false)\r\n    } else if gate_user_msg_type(true, &classify_local_origin(self_id)) == EVENT_TYPE_USER_MSG {\r\n        (EVENT_TYPE_USER_MSG, false)\r\n    } else {\r\n        (EVENT_TYPE_MSG, true) // requested but not user-backed → degraded, loud\r\n    };\r\n    // [impl->REQ-MSG-DELIVERY-AXES]\r\n    match json {\r\n        // json present (non-empty body) → pre-compose the typed envelope WITH the\r\n        // json attr so it rides verbatim. Empty body → no message (json is metadata\r\n        // alongside the body, never a replacement); fall through to the raw path.\r\n        Some(j) if !empty => (\r\n            compose_typed_event(event_type, &[(\"from\", from), (\"json\", j)], body),\r\n            restamped,\r\n        ),\r\n        // No json: preserve the prior behavior exactly.\r\n        _ if event_type == EVENT_TYPE_USER_MSG => (compose_user_msg_event(from, body), false),\r\n        _ => (body.to_string(), restamped),\r\n    }\r\n}\r\n\r\n// The spt-hosted delivery leg (predicate `is_spt_hosted_no_relay` + injector\r\n// `try_spt_hosted_inject`) lives in `spt_daemon::inject` — the ONE shared\r\n// implementation both this local `spt send` path and the cross-node WAN ingress\r\n// (`receive_wan`) call, so they cannot drift (F-023, REQ-WAN-SPT-HOSTED-DELIVERY).\r\n// The CLI keeps its own `spt_daemon::is_running()` fast-path guard before the\r\n// inject (the daemon may legitimately be down for a CLI invocation); the shared\r\n// injector itself does not depend on that seed-control ping.\r\n\r\n#[allow(clippy::too_many_arguments)]\r\nfn cmd_send(\r\n    target: String,\r\n    from: Option<String>,\r\n    idle_only: bool,\r\n    active_only: bool,\r\n    ephemeral: bool,\r\n    prefer_native: bool,\r\n    force_native: bool,\r\n    json_payload: Option<String>,\r\n    user_msg: bool,\r\n) -> i32 {\r\n    // A bare CLI `spt send` with no owning perch composes an empty `from`; stamp\r\n    // `cli@<node-label>` so the receiver never sees a blank sender (REQ-MSG-CLI-ORIGIN).\r\n    // Scoped to `send` (a legal from-less command); `cmd_ring` keeps its NO_SELF refusal.\r\n    // [impl->REQ-MSG-CLI-ORIGIN]\r\n    let from = {\r\n        let f = resolve_from(from);\r\n        if f.is_empty() {\r\n            cli_origin_label()\r\n        } else {\r\n            f\r\n        }\r\n    };\r\n    // Trim stdin like cmd_notify (REQ-HAZARD-ENVELOPE-CR-LINESAFE parity): a\r\n    // Windows `echo | spt send` pipes a trailing CRLF — strip it so the message\r\n    // has no trailing blank line (the codec also neutralizes any interior CR).\r\n    let body = read_stdin();\r\n    let body = body.trim();\r\n    let owlery = perch::owlery_dir();\r\n\r\n    // user-msg identity gate (REQ-MSG-5): the requested type is honored only\r\n    // from a user-backed LOCAL origin — keyed on the *session-proven* self\r\n    // identity ([`roster::detect_self_id`]), never the `--from` reply label.\r\n    // An agent-family sender is re-stamped to plain `msg`, loudly. The honored\r\n    // form is a pre-composed `user-msg` envelope that rides the verbatim\r\n    // typed-envelope rails (spool/TCP/WAN) and renders `<EVENT type=\"user-msg\">`\r\n    // at the receiver; the empty-body refusal is preserved (no compose on empty).\r\n    let (body_to_send, restamped) = apply_user_msg_gate(\r\n        roster::detect_self_id().as_deref(),\r\n        &from,\r\n        body,\r\n        user_msg,\r\n        json_payload.as_deref(),\r\n    );\r\n    if restamped {\r\n        eprintln!(\r\n            \"USER_MSG_RESTAMP:{target}: sender is not a user-backed origin — delivered as plain msg\"\r\n        );\r\n    }\r\n    let body = body_to_send.as_str();\r\n\r\n    // FORCE-NATIVE (ADR-0028 W3) is binary-or-nothing: ONLY a broker inject through\r\n    // the translation binary delivers; any miss is a LOUD not-delivered (honest, no\r\n    // silent drop), never spooled or relayed (the sender explicitly opted into\r\n    // binary-or-nothing — no durable persistence). Native bypasses the idle gate.\r\n    //\r\n    // F029 C-1 SEAM-2 rider: the UNDELIVERED message is DIFFERENTIATED BY LEG so a\r\n    // field repro pins the cause from CLI output alone (the pre-C-1 identical message\r\n    // for every falsy leg made SEAM-2 undiagnosable). `leg=` markers: `empty`\r\n    // (nothing to send); `cli-gate-no-daemon` (no daemon → no broker to inject\r\n    // through); `cli-gate-not-hosted` (the target is not a broker-hosted controllable\r\n    // endpoint — a relay/harness-hosted or non-controllable perch — OR the broker was\r\n    // unreachable); `broker-spooled-active` (the broker HAS the session but the\r\n    // endpoint is ACTIVE mid-turn); `broker-spooled-no-binary` (idle but no working\r\n    // translation binary — faulted/absent, or no hosted session). SEAM-2's own root\r\n    // (the boundary registration/gate loss) is a separate open item.\r\n    // [impl->REQ-MSG-DELIVERY-AXES]\r\n    if force_native {\r\n        if body.is_empty() {\r\n            eprintln!(\r\n                \"FORCE_NATIVE_UNDELIVERED:{target}: empty message (leg=empty) — \\\r\n                 --force-native is binary-or-nothing (not spooled)\"\r\n            );\r\n            return 1;\r\n        }\r\n        if !spt_daemon::is_running() {\r\n            eprintln!(\r\n                \"FORCE_NATIVE_UNDELIVERED:{target}: daemon not running — no broker to \\\r\n                 inject through (leg=cli-gate-no-daemon); --force-native is \\\r\n                 binary-or-nothing (not spooled)\"\r\n            );\r\n            return 1;\r\n        }\r\n        return match spt_daemon::try_spt_hosted_inject(&target, &from, body, &owlery, true) {\r\n            Some((true, _)) => {\r\n                ok_line(format!(\"SENT:{target}\"));\r\n                0\r\n            }\r\n            Some((false, true)) => {\r\n                eprintln!(\r\n                    \"FORCE_NATIVE_UNDELIVERED:{target}: broker spooled — endpoint is \\\r\n                     ACTIVE (mid-turn, no idle inject window) (leg=broker-spooled-active); \\\r\n                     --force-native is binary-or-nothing (not spooled)\"\r\n                );\r\n                1\r\n            }\r\n            Some((false, false)) => {\r\n                eprintln!(\r\n                    \"FORCE_NATIVE_UNDELIVERED:{target}: broker spooled — no working \\\r\n                     translation binary (idle but faulted/absent, or no hosted session) \\\r\n                     (leg=broker-spooled-no-binary); --force-native is binary-or-nothing \\\r\n                     (not spooled)\"\r\n                );\r\n                1\r\n            }\r\n            None => {\r\n                eprintln!(\r\n                    \"FORCE_NATIVE_UNDELIVERED:{target}: not a broker-hosted controllable \\\r\n                     endpoint, or the broker was unreachable (leg=cli-gate-not-hosted); \\\r\n                     --force-native is binary-or-nothing (not spooled)\"\r\n                );\r\n                1\r\n            }\r\n        };\r\n    }\r\n\r\n    // The sender's delivery WINDOW (ADR-0028 W2): default (both windows) /\r\n    // idle_only / active_only — a CALLER-side spool tag deciding which drain surface\r\n    // later picks the row up (active hook poll vs idle/wake relay).\r\n    let window = if active_only {\r\n        spt_store::spool::WINDOW_ACTIVE_ONLY\r\n    } else if idle_only {\r\n        spt_store::spool::WINDOW_IDLE_ONLY\r\n    } else {\r\n        spt_store::spool::WINDOW_DEFAULT\r\n    };\r\n\r\n    // CHANNEL axis (W3) + spt-hosted PTY-inject (REQ-SEND-SPT-HOSTED): an spt-hosted\r\n    // endpoint (broker holds its PTY) injects the rendered <EVENT> when the broker\r\n    // delivers. `--prefer-native` injects through the binary REGARDLESS of idle/active\r\n    // (native bypasses the gate, `native=true`), falling back to the normal\r\n    // spool-per-window on a miss. A non-native default/idle_only send injects only\r\n    // when the broker says the target is idle (the activity gate, `native=false`).\r\n    // A non-native `--active-only` is the explicit hook channel: spool-only, no inject\r\n    // attempt. Any miss spools tagged with the window (never lost).\r\n    // [impl->REQ-MSG-DELIVERY-AXES]\r\n    let try_inject = prefer_native || !active_only;\r\n    let outcome = if try_inject && !body.is_empty() && spt_daemon::is_running() {\r\n        match spt_daemon::try_spt_hosted_inject(&target, &from, body, &owlery, prefer_native) {\r\n            Some((true, _)) => SendOutcome::Sent,\r\n            Some((false, _)) | None => {\r\n                deliver::send_windowed(&target, &from, body, &owlery, window, ephemeral)\r\n            }\r\n        }\r\n    } else {\r\n        deliver::send_windowed(&target, &from, body, &owlery, window, ephemeral)\r\n    };\r\n\r\n    // REQ-IDLE-PARKED-DELIVERY (primary — send-time): a non-active-only send that spooled\r\n    // to an ALREADY-idle spt-hosted target will never get an ACTIVE→IDLE edge (the F-023\r\n    // idle-edge drain only fires on the transition), so it would park until an operator\r\n    // poke. Re-offer the just-spooled row through the inject leg NOW; the pulse-tick belt\r\n    // is the backstop. On a successful inject the send is honestly SENT, not QUEUED.\r\n    // [impl->REQ-IDLE-PARKED-DELIVERY]\r","truncated":true,"truncatedBy":"lines","totalLines":12482,"totalBytes":9335,"outputLines":190,"outputBytes":9335,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn apply_user_msg_gate(\n…\n    json: Option<&str>,\r\n) -> (String, bool) {\r\n    let empty = body.trim().is_empty();\r\n    // The user-authority type decision (unchanged security gate): honored only from\r\n    // a user-backed local origin; an agent-family sender is re-stamped to plain msg.\r\n    let (event_type, restamped) = if !requested || empty {\r\n        (EVENT_TYPE_MSG, false)\r\n    } else if gate_user_msg_type(true, &classify_local_origin(self_id)) == EVENT_TYPE_USER_MSG {\r\n        (EVENT_TYPE_USER_MSG, false)\r\n    } else {\r\n        (EVENT_TYPE_MSG, true) // requested but not user-backed → degraded, loud\r\n    };\r\n    // [impl->REQ-MSG-DELIVERY-AXES]\r\n    match json {\r\n        // json present (non-empty body) → pre-compose the typed envelope WITH the\r\n        // json attr so it rides verbatim. Empty body → no message (json is metadata\r\n        // alongside the body, never a replacement); fall through to the raw path.\r\n        Some(j) if !empty => (\r\n            compose_typed_event(event_type, &[(\"from\", from), (\"json\", j)], body),\r\n            restamped,\r\n        ),\r\n        // No json: preserve the prior behavior exactly.\r\n        _ if event_type == EVENT_TYPE_USER_MSG => (compose_user_msg_event(from, body), false),\r\n        _ => (body.to_string(), restamped),\r\n    }\r\n}\r\n\r\n// The spt-hosted delivery leg (predicate `is_spt_hosted_no_relay` + injector\r\n// `try_spt_hosted_inject`) lives in `spt_daemon::inject` — the ONE shared\r\n// implementation both this local `spt send` path and the cross-node WAN ingress\r\n// (`receive_wan`) call, so they cannot drift (F-023, REQ-WAN-SPT-HOSTED-DELIVERY).\r\n// The CLI keeps its own `spt_daemon::is_running()` fast-path guard before the\r\n// inject (the daemon may legitimately be down for a CLI invocation); the shared\r\n// injector itself does not depend on that seed-control ping.\r\n\r\n#[allow(clippy::too_many_arguments)]\r\nfn cmd_send(\r\n    target: String,\r\n    from: Option<String>,\r\n    idle_only: bool,\r\n    active_only: bool,\r\n    ephemeral: bool,\r\n    prefer_native: bool,\r\n    force_native: bool,\r\n    json_payload: Option<String>,\r\n    user_msg: bool,\r\n) -> i32 {\r\n    // A bare CLI `spt send` with no owning perch composes an empty `from`; stamp\r\n    // `cli@<node-label>` so the receiver never sees a blank sender (REQ-MSG-CLI-ORIGIN).\r\n    // Scoped to `send` (a legal from-less command); `cmd_ring` keeps its NO_SELF refusal.\r\n    // [impl->REQ-MSG-CLI-ORIGIN]\r\n    let from = {\r\n        let f = resolve_from(from);\r\n        if f.is_empty() {\r\n            cli_origin_label()\r\n        } else {\r\n            f\r\n        }\r\n    };\r\n    // Trim stdin like cmd_notify (REQ-HAZARD-ENVELOPE-CR-LINESAFE parity): a\r\n    // Windows `echo | spt send` pipes a trailing CRLF — strip it so the message\r\n    // has no trailing blank line (the codec also neutralizes any interior CR).\r\n    let body = read_stdin();\r\n    let body = body.trim();\r\n    let owlery = perch::owlery_dir();\r\n\r\n    // user-msg identity gate (REQ-MSG-5): the requested type is honored only\r\n    // from a user-backed LOCAL origin — keyed on the *session-proven* self\r\n    // identity ([`roster::detect_self_id`]), never the `--from` reply label.\r\n    // An agent-family sender is re-stamped to plain `msg`, loudly. The honored\r\n    // form is a pre-composed `user-msg` envelope that rides the verbatim\r\n    // typed-envelope rails (spool/TCP/WAN) and renders `<EVENT type=\"user-msg\">`\r\n    // at the receiver; the empty-body refusal is preserved (no compose on empty).\r\n    let (body_to_send, restamped) = apply_user_msg_gate(\r\n        roster::detect_self_id().as_deref(),\r\n        &from,\r\n        body,\r\n        user_msg,\r\n        json_payload.as_deref(),\r\n    );\r\n    if restamped {\r\n        eprintln!(\r\n            \"USER_MSG_RESTAMP:{target}: sender is not a user-backed origin — delivered as plain msg\"\r\n        );\r\n    }\r\n    let body = body_to_send.as_str();\r\n\r\n    // FORCE-NATIVE (ADR-0028 W3) is binary-or-nothing: ONLY a broker inject through\r\n    // the translation binary delivers; any miss is a LOUD not-delivered (honest, no\r\n    // silent drop), never spooled or relayed (the sender explicitly opted into\r\n    // binary-or-nothing — no durable persistence). Native bypasses the idle gate.\r\n    //\r\n    // F029 C-1 SEAM-2 rider: the UNDELIVERED message is DIFFERENTIATED BY LEG so a\r\n    // field repro pins the cause from CLI output alone (the pre-C-1 identical message\r\n    // for every falsy leg made SEAM-2 undiagnosable). `leg=` markers: `empty`\r\n    // (nothing to send); `cli-gate-no-daemon` (no daemon → no broker to inject\r\n    // through); `cli-gate-not-hosted` (the target is not a broker-hosted controllable\r\n    // endpoint — a relay/harness-hosted or non-controllable perch — OR the broker was\r\n    // unreachable); `broker-spooled-active` (the broker HAS the session but the\r\n    // endpoint is ACTIVE mid-turn); `broker-spooled-no-binary` (idle but no working\r\n    // translation binary — faulted/absent, or no hosted session). SEAM-2's own root\r\n    // (the boundary registration/gate loss) is a separate open item.\r\n    // [impl->REQ-MSG-DELIVERY-AXES]\r\n    if force_native {\r\n        if body.is_empty() {\r\n            eprintln!(\r\n                \"FORCE_NATIVE_UNDELIVERED:{target}: empty message (leg=empty) — \\\r\n                 --force-native is binary-or-nothing (not spooled)\"\r\n            );\r\n            return 1;\r\n        }\r\n        if !spt_daemon::is_running() {\r\n            eprintln!(\r\n                \"FORCE_NATIVE_UNDELIVERED:{target}: daemon not running — no broker to \\\r\n                 inject through (leg=cli-gate-no-daemon); --force-native is \\\r\n                 binary-or-nothing (not spooled)\"\r\n            );\r\n            return 1;\r\n        }\r\n        return match spt_daemon::try_spt_hosted_inject(&target, &from, body, &owlery, true) {\r\n            Some((true, _)) => {\r\n                ok_line(format!(\"SENT:{target}\"));\r\n                0\r\n            }\r\n            Some((false, true)) => {\r\n                eprintln!(\r\n                    \"FORCE_NATIVE_UNDELIVERED:{target}: broker spooled — endpoint is \\\r\n                     ACTIVE (mid-turn, no idle inject window) (leg=broker-spooled-active); \\\r\n                     --force-native is binary-or-nothing (not spooled)\"\r\n                );\r\n                1\r\n            }\r\n            Some((false, false)) => {\r\n                eprintln!(\r\n                    \"FORCE_NATIVE_UNDELIVERED:{target}: broker spooled — no working \\\r\n                     translation binary (idle but faulted/absent, or no hosted session) \\\r\n                     (leg=broker-spooled-no-binary); --force-native is binary-or-nothing \\\r\n                     (not spooled)\"\r\n                );\r\n                1\r\n            }\r\n            None => {\r\n                eprintln!(\r\n                    \"FORCE_NATIVE_UNDELIVERED:{target}: not a broker-hosted controllable \\\r\n                     endpoint, or the broker was unreachable (leg=cli-gate-not-hosted); \\\r\n                     --force-native is binary-or-nothing (not spooled)\"\r\n                );\r\n                1\r\n            }\r\n        };\r\n    }\r\n\r\n    // The sender's delivery WINDOW (ADR-0028 W2): default (both windows) /\r\n    // idle_only / active_only — a CALLER-side spool tag deciding which drain surface\r\n    // later picks the row up (active hook poll vs idle/wake relay).\r\n    let window = if active_only {\r\n        spt_store::spool::WINDOW_ACTIVE_ONLY\r\n    } else if idle_only {\r\n        spt_store::spool::WINDOW_IDLE_ONLY\r\n    } else {\r\n        spt_store::spool::WINDOW_DEFAULT\r\n    };\r\n\r\n    // CHANNEL axis (W3) + spt-hosted PTY-inject (REQ-SEND-SPT-HOSTED): an spt-hosted\r\n    // endpoint (broker holds its PTY) injects the rendered <EVENT> when the broker\r\n    // delivers. `--prefer-native` injects through the binary REGARDLESS of idle/active\r\n    // (native bypasses the gate, `native=true`), falling back to the normal\r\n    // spool-per-window on a miss. A non-native default/idle_only send injects only\r\n    // when the broker says the target is idle (the activity gate, `native=false`).\r\n    // A non-native `--active-only` is the explicit hook channel: spool-only, no inject\r\n    // attempt. Any miss spools tagged with the window (never lost).\r\n    // [impl->REQ-MSG-DELIVERY-AXES]\r\n    let try_inject = prefer_native || !active_only;\r\n    let outcome = if try_inject && !body.is_empty() && spt_daemon::is_running() {\r\n        match spt_daemon::try_spt_hosted_inject(&target, &from, body, &owlery, prefer_native) {\r\n            Some((true, _)) => SendOutcome::Sent,\r\n            Some((false, _)) | None => {\r\n                deliver::send_windowed(&target, &from, body, &owlery, window, ephemeral)\r\n            }\r\n        }\r\n    } else {\r\n        deliver::send_windowed(&target, &from, body, &owlery, window, ephemeral)\r\n    };\r\n\r\n    // REQ-IDLE-PARKED-DELIVERY (primary — send-time): a non-active-only send that spooled\r\n    // to an ALREADY-idle spt-hosted target will never get an ACTIVE→IDLE edge (the F-023\r\n    // idle-edge drain only fires on the transition), so it would park until an operator\r\n    // poke. Re-offer the just-spooled row through the inject leg NOW; the pulse-tick belt\r\n    // is the backstop. On a successful inject the send is honestly SENT, not QUEUED.\r\n    // [impl->REQ-IDLE-PARKED-DELIVERY]\r\n…\n}","startLine":6414,"lineNumbers":[6414,null,6419,6420,6421,6422,6423,6424,6425,6426,6427,6428,6429,6430,6431,6432,6433,6434,6435,6436,6437,6438,6439,6440,6441,6442,6443,6444,6445,6446,6447,6448,6449,6450,6451,6452,6453,6454,6455,6456,6457,6458,6459,6460,6461,6462,6463,6464,6465,6466,6467,6468,6469,6470,6471,6472,6473,6474,6475,6476,6477,6478,6479,6480,6481,6482,6483,6484,6485,6486,6487,6488,6489,6490,6491,6492,6493,6494,6495,6496,6497,6498,6499,6500,6501,6502,6503,6504,6505,6506,6507,6508,6509,6510,6511,6512,6513,6514,6515,6516,6517,6518,6519,6520,6521,6522,6523,6524,6525,6526,6527,6528,6529,6530,6531,6532,6533,6534,6535,6536,6537,6538,6539,6540,6541,6542,6543,6544,6545,6546,6547,6548,6549,6550,6551,6552,6553,6554,6555,6556,6557,6558,6559,6560,6561,6562,6563,6564,6565,6566,6567,6568,6569,6570,6571,6572,6573,6574,6575,6576,6577,6578,6579,6580,6581,6582,6583,6584,6585,6586,6587,6588,6589,6590,6591,6592,6593,6594,6595,6596,6597,6598,6599,6600,6601,6602,6603,6604,6605,6606,6607,6608,null,6696]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":18900,"totalBytes":9335,"outputLines":190,"outputBytes":9335,"shownRange":{"start":6419,"end":6608},"nextOffset":6609}}},"isError":false,"timestamp":1784197478017}}
{"type":"message","id":"6394338c","parentId":"ec5bb4df","timestamp":"2026-07-16T10:24:47.222Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Continuing result and WAN fallback**"},{"type":"toolCall","id":"call_MvMbCSenuNZDrzNf2zIdnA3E|fc_017bff223290b7ba016a58b16f3270819ab9be757244ce7c95","name":"read","arguments":{"i":"Reading send outcome and WAN fallback","path":"crates/spt/src/cli.rs","selector":"6600-6715"}},{"type":"toolCall","id":"call_S6QdmFKNSNnotwZ8ZejJNokO|fc_017bff223290b7ba016a58b16f327c819a8a2312703f4d584d","name":"read","arguments":{"i":"Reading WAN send core","path":"crates/spt/src/wansend.rs","selector":"180-290"}},{"type":"toolCall","id":"call_OpzY34nCKDhylty37br6pRzn|fc_017bff223290b7ba016a58b16f3280819aa9f892b7011c034a","name":"read","arguments":{"i":"Reading liveness predicates","path":"crates/spt-store/src/liveness.rs","selector":"1-210"}},{"type":"toolCall","id":"call_6c488zycC7ITG5e4kpovvLga|fc_017bff223290b7ba016a58b16f3288819abcb306864126f3b7","name":"grep","arguments":{"i":"Finding messaging integration fixtures","pattern":"hyphen|listen.*send|send.*listen|PollListener|Relay::bind|SENT:|QUEUED:","path":"crates/spt/tests; crates/spt-msg/src; crates/spt-daemon/tests","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_bAklXmShNsKVbGZZom2flVX1|fc_017bff223290b7ba016a58b16f328c819a8051514240668dbc","name":"grep","arguments":{"i":"Finding CLI target parser tests","pattern":"Send \\{|target:|hyphen|trailing_var_arg|allow_hyphen_values|last = true|num_args","path":"crates/spt/src/cli.rs; crates/spt/tests","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":21158,"output":301,"cacheRead":164352,"cacheWrite":0,"totalTokens":185811,"reasoningTokens":10,"cost":{"input":0.10579000000000001,"output":0.00903,"cacheRead":0.082176,"cacheWrite":0,"total":0.196996}},"stopReason":"toolUse","timestamp":1784197478063,"responseId":"resp_017bff223290b7ba016a58b16670c4819a97f6cc7833b7c5a2","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b169b4a4819a8d581cc272467b8b","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLFvIqWqboEVHspw_IKJQAOBXP4Fw0Aeg2ApQuz9MqU5no6aLoVfI3ntJL-3-7QTHIpe8VLu-VcYNJ-HShsV0wQuaxbELFahTpHHiQQLRl1K95lwlWJKsE8XkcejlUx1PjFXjTSawTgUTk1WWD6vHcn_Z2jt9B90EM7AkrUWsbwqsnX6a4s2jq3NXeDRuozVIHO6HEDQ1qrTG05SswihyqAwvkNS03zJe9-yQwvDM4XTmvg2pjsRk0_n8iC5vLkmPRWjcGaH_9cTumszcD_Z6zmX-OMzZVRNJgDtEBJ68AOke8a3bsg5cYTYxhX1J7dMewZvagA9kqcw8fqRLa727c4QoMGSH3sIxGldLCIQFkREvI_7GIl0F1jPhDwTehSPWJfG9F9ZvNAleA5NKBfpSSPTVb0W_3s7k9wNM4Ck9b_amyaBXTfuJskyCpMCiRgaP5Z3wfBGcL8LyvuPIlqNdRvQYixbcPrp_ik6z16Zuz-ZWRjd0saqZ8p8xKi_bz7o-ZV4j75zlXWdbjMl8BVCatHKnfKtzWJ0gnopRrI8zXxEri5SpzTqd61xAOIom-4jEkOrTBHUfG-pk6BALNsnfDgHgWQD4yD2xtwIjBYqRGLHFLoW0PEKLvP3JJmfHa3OtCe6GZuS7uXNZQNKlJnzcoseZFmAH5LhNi-XN18XvgO-tKCSLG44XjvHbvqAMpL5whZXUOEPMFHlOyVP1Q6kJ5oRxmkFWyocAKns8AbxOb2q7wCFshAAXmU4Zf_anpdPJEyA5_ufpWFc7LlGDSUSs2PQPhDOcc611ckLbrPPcQkm7QgguMA60iQYMjqg0W57GWwD3uugo68muT2f_B_nKdzySQnb9ESXVQP13OIceMQEt0jpfsIXoXj6Tdr7o5k9fuB0S_R76172yS2-kXFzsh-Y3YGodaMErSnpgT4fUC5rvIa1Qqn-m21kexmCOSA3tq6u2byZh2ApsfmSrn4oGes8DA==","summary":[{"type":"summary_text","text":"**Continuing result and WAN fallback**"}]},{"id":"fc_017bff223290b7ba016a58b16f3270819ab9be757244ce7c95","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading send outcome and WAN fallback\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"6600-6715\"}","call_id":"call_MvMbCSenuNZDrzNf2zIdnA3E","name":"read"},{"id":"fc_017bff223290b7ba016a58b16f327c819a8a2312703f4d584d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading WAN send core\",\"path\":\"crates/spt/src/wansend.rs\",\"selector\":\"180-290\"}","call_id":"call_S6QdmFKNSNnotwZ8ZejJNokO","name":"read"},{"id":"fc_017bff223290b7ba016a58b16f3280819aa9f892b7011c034a","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading liveness predicates\",\"path\":\"crates/spt-store/src/liveness.rs\",\"selector\":\"1-210\"}","call_id":"call_OpzY34nCKDhylty37br6pRzn","name":"read"},{"id":"fc_017bff223290b7ba016a58b16f3288819abcb306864126f3b7","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding messaging integration fixtures\",\"pattern\":\"hyphen|listen.*send|send.*listen|PollListener|Relay::bind|SENT:|QUEUED:\",\"path\":\"crates/spt/tests; crates/spt-msg/src; crates/spt-daemon/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_6c488zycC7ITG5e4kpovvLga","name":"grep"},{"id":"fc_017bff223290b7ba016a58b16f328c819a8051514240668dbc","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding CLI target parser tests\",\"pattern\":\"Send \\\\{|target:|hyphen|trailing_var_arg|allow_hyphen_values|last = true|num_args\",\"path\":\"crates/spt/src/cli.rs; crates/spt/tests\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_bAklXmShNsKVbGZZom2flVX1","name":"grep"}]},"duration":9158.114500000142,"ttft":3616.1187000004575,"contextSnapshot":{"promptTokens":185510,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_MvMbCSenuNZDrzNf2zIdnA3E|fc_017bff223290b7ba016a58b16f3270819ab9be757244ce7c95","toolName":"read","startedAt":"2026-07-16T10:24:47.222Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading send outcome and WAN fallback"},"id":"cacd38df","parentId":"6394338c","timestamp":"2026-07-16T10:24:47.222Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_S6QdmFKNSNnotwZ8ZejJNokO|fc_017bff223290b7ba016a58b16f327c819a8a2312703f4d584d","toolName":"read","startedAt":"2026-07-16T10:24:47.222Z","args":{"path":"crates/spt/src/wansend.rs"},"intent":"Reading WAN send core"},"id":"995a1712","parentId":"cacd38df","timestamp":"2026-07-16T10:24:47.222Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OpzY34nCKDhylty37br6pRzn|fc_017bff223290b7ba016a58b16f3280819aa9f892b7011c034a","toolName":"read","startedAt":"2026-07-16T10:24:47.222Z","args":{"path":"crates/spt-store/src/liveness.rs"},"intent":"Reading liveness predicates"},"id":"a17b2a5d","parentId":"995a1712","timestamp":"2026-07-16T10:24:47.222Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_6c488zycC7ITG5e4kpovvLga|fc_017bff223290b7ba016a58b16f3288819abcb306864126f3b7","toolName":"grep","startedAt":"2026-07-16T10:24:47.222Z","args":{"path":"crates/spt/tests; crates/spt-msg/src; crates/spt-daemon/tests"},"intent":"Finding messaging integration fixtures"},"id":"5ef76b13","parentId":"a17b2a5d","timestamp":"2026-07-16T10:24:47.222Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_bAklXmShNsKVbGZZom2flVX1|fc_017bff223290b7ba016a58b16f328c819a8051514240668dbc","toolName":"grep","startedAt":"2026-07-16T10:24:47.222Z","args":{"path":"crates/spt/src/cli.rs; crates/spt/tests"},"intent":"Finding CLI target parser tests"},"id":"3f8272aa","parentId":"5ef76b13","timestamp":"2026-07-16T10:24:47.222Z"}
{"type":"message","id":"3709ebcd","parentId":"3f8272aa","timestamp":"2026-07-16T10:24:47.233Z","message":{"role":"toolResult","toolCallId":"call_OpzY34nCKDhylty37br6pRzn|fc_017bff223290b7ba016a58b16f3280819aa9f892b7011c034a","toolName":"read","content":[{"type":"text","text":"//! **The** single liveness resolver — daemon-authoritative for daemon-hosted\n//! perches, per-pid for everything else (KNOWN-HAZARDS 2.5,\n//! REQ-HAZARD-DAEMON-HOSTED-LIVENESS).\n//!\n//! ## Why one resolver\n//! The sister project, and the spt-core **interim** (M1/M2a/M2b) model, evaluate\n//! a perch's liveness from its owning process pid: `is_process_alive(info.pid)`,\n//! with `clean_stale_entries` dropping dead-pid registry rows. That is correct\n//! while every perch has a *dedicated* per-process owner.\n//!\n//! Under ADR-0004 the daemon hosts the Psyche (and any spt-hosted Self) as a\n//! **loop inside the daemon**, not a separate process. Such a perch holds no\n//! dedicated pid; its `claude`/summarizer subprocess is **ephemeral** (spawned\n//! per pulse/commune, then exits). If liveness keyed on that pid, killing the\n//! summarizer would read the whole perch \"offline\"; if it keyed on the daemon's\n//! pid, every hosted endpoint would share one pid and `clean_stale_entries`\n//! could never distinguish a torn-down endpoint from a live one (2.5).\n//!\n//! So for **daemon-hosted** perches liveness is the daemon-managed `status` field\n//! on `info.json` ([`crate::info::set_status`]); for **external/interim** perches\n//! it stays the per-pid probe. Both decisions live **here**, behind one resolver\n//! (mirroring how [`crate::registry::resolve_address`] centralizes stale-clean),\n//! so the daemon swap is localized and the per-pid assumption never leaks into a\n//! new call site. The three call sites — `deliver::is_online`,\n//! `ring::from_has_live_perch`, `roster::enumerate` (perch path) and\n//! `registry::clean_stale_entries` (registry row) — all route through this module.\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n\nuse std::path::Path;\n\nuse serde_json::Value;\n\nuse crate::perch::{self, ParentHint};\nuse crate::proc::is_process_alive;\n\n/// Daemon-managed `status` value meaning the hosted endpoint is live.\npub const STATUS_ONLINE: &str = \"online\";\n/// Daemon-managed `status` value meaning the hosted endpoint is torn down.\npub const STATUS_OFFLINE: &str = \"offline\";\n/// Daemon-managed `status` value: a skeleton perch was created (harness spawned)\n/// but `spt api bind` has not yet been called. The endpoint is broker-attachable\n/// (a live PTY session exists) but NOT message-addressable (no `session_id` yet).\n/// Lifecycle: spawn/skeleton-create → `unbound`; bind → `online`; session death\n/// → `offline` via the B2 reconcile.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub const STATUS_UNBOUND: &str = \"unbound\";\n\n/// Torn-read retry budget (mirrors [`crate::info`]): a reader may hit the file\n/// mid-replace; retry a few times, then degrade rather than panic.\nconst READ_ATTEMPTS: u32 = 5;\n\n/// The outcome of a tolerant `info.json` read, distinguishing the two failure\n/// modes a liveness decision must NOT conflate (REQ-HAZARD-CORRUPT-PERCH-COHERENCE):\n/// - [`Present`](RawRead::Present): parsed a value (possibly schema-minimal).\n/// - [`Absent`](RawRead::Absent): no file — the perch dir carries no record (or the\n///   read never got past a non-existence error). Interim parity treats this as alive.\n/// - [`Corrupt`](RawRead::Corrupt): the file EXISTS but stays unparseable across every\n///   retry — a destroyed record (e.g. a hard-reset NUL zero-fill). NOT alive: a wiped\n///   record must never read as a live endpoint.\nenum RawRead {\n    Present(Value),\n    Absent,\n    Corrupt,\n}\n\n/// One tolerant read of a perch's `info.json` as a raw value — schema-minimal\n/// records (`{\"pid\": N}` from wrapper-owned listeners) parse where the full\n/// [`crate::info::InfoJson`] shape would not. Distinguishes ABSENT (no file) from\n/// CORRUPT (file present, persistently unparseable) — a mid-replace torn read is\n/// absorbed by the retry loop (the atomic rename means a retry sees the complete\n/// old or new file), so only a genuinely destroyed record degrades to `Corrupt`.\nfn read_raw_state(perch_path: &Path) -> RawRead {\n    let path = perch_path.join(\"info.json\");\n    let mut existed_unparseable = false;\n    for _ in 0..READ_ATTEMPTS {\n        match std::fs::read_to_string(&path) {\n            Ok(content) => match serde_json::from_str::<Value>(&content) {\n                Ok(val) => return RawRead::Present(val),\n                Err(_) => existed_unparseable = true, // present but bad — retry (mid-replace?)\n            },\n            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return RawRead::Absent,\n            Err(_) => {} // transient sharing/access error — retry\n        }\n    }\n    if existed_unparseable {\n        RawRead::Corrupt\n    } else {\n        RawRead::Absent\n    }\n}\n\n/// [`read_raw_state`] collapsed to an `Option` for callers that treat absent and\n/// corrupt identically (status/unbound lookups fall through to the external path\n/// for either). Liveness-deciding callers use [`read_raw_state`] directly so a\n/// corrupt record is not mistaken for a live one.\nfn read_raw(perch_path: &Path) -> Option<Value> {\n    match read_raw_state(perch_path) {\n        RawRead::Present(v) => Some(v),\n        RawRead::Absent | RawRead::Corrupt => None,\n    }\n}\n\n/// The daemon-managed `status` of a perch, if it is daemon-hosted. `Some(_)` ⇒\n/// daemon-hosted (liveness is authoritative via status); `None` ⇒ external /\n/// interim (liveness is the per-pid probe).\nfn read_status(perch_path: &Path) -> Option<String> {\n    read_raw(perch_path)?\n        .get(\"status\")\n        .and_then(|v| v.as_str())\n        .map(str::to_string)\n}\n\n/// True when a perch is in the `unbound` state (skeleton created, harness not\n/// yet called `spt api bind`). The endpoint is broker-attachable but NOT\n/// message-addressable — `is_perch_alive` stays `false` for it.\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn is_perch_unbound(perch_path: &Path) -> bool {\n    read_status(perch_path).as_deref() == Some(STATUS_UNBOUND)\n}\n\n/// Liveness for a perch by its **directory**. THE resolver for the path-aware\n/// call sites (`is_online`, `from_has_live_perch`, roster).\n///\n/// - **Daemon-hosted** (`status` present): live iff `status == \"online\"`. The\n///   `info.pid` (an ephemeral summarizer pid, or the shared daemon pid) is\n///   **never** consulted — killing the summarizer does not flip the perch.\n/// - **External/interim** (`status` absent): the per-pid probe, preserving the\n///   exact interim semantics — a numeric pid is probed; a `\"BUSY\"` sentinel or\n///   unparseable/absent pid reads alive (the listener is up between polls).\n/// - **Corrupt** (file present, persistently unparseable): NOT alive\n///   (REQ-HAZARD-CORRUPT-PERCH-COHERENCE) — a destroyed record (e.g. a hard-reset\n///   NUL zero-fill) must never read as a live endpoint. This is the ONE departure\n///   from interim parity: ABSENT stays alive, CORRUPT does not.\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n// [impl->REQ-HAZARD-CORRUPT-PERCH-COHERENCE]\npub fn is_perch_alive(perch_path: &Path) -> bool {\n    match read_raw_state(perch_path) {\n        RawRead::Present(val) => {\n            if let Some(status) = val.get(\"status\").and_then(|v| v.as_str()) {\n                return status == STATUS_ONLINE; // daemon-authoritative; never pid\n            }\n            match val.get(\"pid\").and_then(|v| v.as_u64()) {\n                Some(pid) => is_process_alive(pid as u32),\n                None => true, // BUSY sentinel / unparseable pid — interim parity\n            }\n        }\n        RawRead::Absent => true, // absent info.json — interim parity (read_pid None ⇒ alive)\n        RawRead::Corrupt => false, // destroyed record is NOT a live endpoint\n    }\n}\n\n/// Liveness for a **registry row**: its id plus the pid it was registered under,\n/// resolved within `owlery` (the registry's own root). Daemon-hosted rows key on\n/// the endpoint `status`; external rows pid-probe the **row's** pid exactly as\n/// the interim `clean_stale_entries` did — so the daemon swap touches only the\n/// hosted rows. An offline daemon-hosted endpoint is cleaned even though the\n/// (shared) daemon pid is still alive; a still-online one survives a dead\n/// ephemeral pid.\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\npub fn is_registry_entry_alive(id: &str, registry_pid: u32, owlery: &Path) -> bool {\n    let perch_path = perch::resolve_perch_path_in(owlery, id, ParentHint::Infer);\n    match read_status(&perch_path) {\n        // `unbound` is a live skeleton — don't clean it; `online` is live; anything\n        // else (offline, unknown) is stale → clean.\n        Some(status) => status == STATUS_ONLINE || status == STATUS_UNBOUND,\n        None => is_process_alive(registry_pid), // external/interim: pid-probe the row\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::info::{set_status, write_info, InfoJson};\n\n    /// A pid that is never a real allocation (above Linux pid_max ~4.2M and\n    /// Windows ranges; positive as i32). Deterministic — avoids pid-reuse flake.\n    const DEAD_PID: u32 = 2_000_000_000;\n\n    fn perch() -> tempfile::TempDir {\n        tempfile::tempdir().unwrap()\n    }\n\n    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked\n    // `online` reads alive even though its recorded (summarizer) pid is DEAD —\n    // killing the summarizer subprocess does not flip the perch offline.\n    #[test]\n    fn daemon_hosted_online_survives_dead_pid() {\n        let d = perch();\n        // Record the ephemeral summarizer pid (now dead) then mark daemon-online.\n        write_info(\n            d.path(),\n            &InfoJson::new(\"doyle-psyche\", \"t\", DEAD_PID, \"sid\", \"psyche\"),\n        )\n        .unwrap();\n        set_status(d.path(), STATUS_ONLINE).unwrap();\n        assert!(\n            is_perch_alive(d.path()),\n            \"online status is authoritative; the dead pid is ignored\"\n        );\n    }\n\n    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked\n    // `offline` reads dead even though its recorded pid is ALIVE (this process).\n    #[test]\n    fn daemon_hosted_offline_overrides_live_pid() {\n        let d = perch();\n        write_info(\n            d.path(),\n            &InfoJson::new(\"doyle-psyche\", \"t\", std::process::id(), \"sid\", \"psyche\"),\n        )\n        .unwrap();\n        set_status(d.path(), STATUS_OFFLINE).unwrap();\n…\n    }\n…\n}\n\n[Showing lines 1-213 of 392. Use :214 to continue]"}],"details":{"truncation":{"content":"//! **The** single liveness resolver — daemon-authoritative for daemon-hosted\r\n//! perches, per-pid for everything else (KNOWN-HAZARDS 2.5,\r\n//! REQ-HAZARD-DAEMON-HOSTED-LIVENESS).\r\n//!\r\n//! ## Why one resolver\r\n//! The sister project, and the spt-core **interim** (M1/M2a/M2b) model, evaluate\r\n//! a perch's liveness from its owning process pid: `is_process_alive(info.pid)`,\r\n//! with `clean_stale_entries` dropping dead-pid registry rows. That is correct\r\n//! while every perch has a *dedicated* per-process owner.\r\n//!\r\n//! Under ADR-0004 the daemon hosts the Psyche (and any spt-hosted Self) as a\r\n//! **loop inside the daemon**, not a separate process. Such a perch holds no\r\n//! dedicated pid; its `claude`/summarizer subprocess is **ephemeral** (spawned\r\n//! per pulse/commune, then exits). If liveness keyed on that pid, killing the\r\n//! summarizer would read the whole perch \"offline\"; if it keyed on the daemon's\r\n//! pid, every hosted endpoint would share one pid and `clean_stale_entries`\r\n//! could never distinguish a torn-down endpoint from a live one (2.5).\r\n//!\r\n//! So for **daemon-hosted** perches liveness is the daemon-managed `status` field\r\n//! on `info.json` ([`crate::info::set_status`]); for **external/interim** perches\r\n//! it stays the per-pid probe. Both decisions live **here**, behind one resolver\r\n//! (mirroring how [`crate::registry::resolve_address`] centralizes stale-clean),\r\n//! so the daemon swap is localized and the per-pid assumption never leaks into a\r\n//! new call site. The three call sites — `deliver::is_online`,\r\n//! `ring::from_has_live_perch`, `roster::enumerate` (perch path) and\r\n//! `registry::clean_stale_entries` (registry row) — all route through this module.\r\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n\r\nuse std::path::Path;\r\n\r\nuse serde_json::Value;\r\n\r\nuse crate::perch::{self, ParentHint};\r\nuse crate::proc::is_process_alive;\r\n\r\n/// Daemon-managed `status` value meaning the hosted endpoint is live.\r\npub const STATUS_ONLINE: &str = \"online\";\r\n/// Daemon-managed `status` value meaning the hosted endpoint is torn down.\r\npub const STATUS_OFFLINE: &str = \"offline\";\r\n/// Daemon-managed `status` value: a skeleton perch was created (harness spawned)\r\n/// but `spt api bind` has not yet been called. The endpoint is broker-attachable\r\n/// (a live PTY session exists) but NOT message-addressable (no `session_id` yet).\r\n/// Lifecycle: spawn/skeleton-create → `unbound`; bind → `online`; session death\r\n/// → `offline` via the B2 reconcile.\r\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\npub const STATUS_UNBOUND: &str = \"unbound\";\r\n\r\n/// Torn-read retry budget (mirrors [`crate::info`]): a reader may hit the file\r\n/// mid-replace; retry a few times, then degrade rather than panic.\r\nconst READ_ATTEMPTS: u32 = 5;\r\n\r\n/// The outcome of a tolerant `info.json` read, distinguishing the two failure\r\n/// modes a liveness decision must NOT conflate (REQ-HAZARD-CORRUPT-PERCH-COHERENCE):\r\n/// - [`Present`](RawRead::Present): parsed a value (possibly schema-minimal).\r\n/// - [`Absent`](RawRead::Absent): no file — the perch dir carries no record (or the\r\n///   read never got past a non-existence error). Interim parity treats this as alive.\r\n/// - [`Corrupt`](RawRead::Corrupt): the file EXISTS but stays unparseable across every\r\n///   retry — a destroyed record (e.g. a hard-reset NUL zero-fill). NOT alive: a wiped\r\n///   record must never read as a live endpoint.\r\nenum RawRead {\r\n    Present(Value),\r\n    Absent,\r\n    Corrupt,\r\n}\r\n\r\n/// One tolerant read of a perch's `info.json` as a raw value — schema-minimal\r\n/// records (`{\"pid\": N}` from wrapper-owned listeners) parse where the full\r\n/// [`crate::info::InfoJson`] shape would not. Distinguishes ABSENT (no file) from\r\n/// CORRUPT (file present, persistently unparseable) — a mid-replace torn read is\r\n/// absorbed by the retry loop (the atomic rename means a retry sees the complete\r\n/// old or new file), so only a genuinely destroyed record degrades to `Corrupt`.\r\nfn read_raw_state(perch_path: &Path) -> RawRead {\r\n    let path = perch_path.join(\"info.json\");\r\n    let mut existed_unparseable = false;\r\n    for _ in 0..READ_ATTEMPTS {\r\n        match std::fs::read_to_string(&path) {\r\n            Ok(content) => match serde_json::from_str::<Value>(&content) {\r\n                Ok(val) => return RawRead::Present(val),\r\n                Err(_) => existed_unparseable = true, // present but bad — retry (mid-replace?)\r\n            },\r\n            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return RawRead::Absent,\r\n            Err(_) => {} // transient sharing/access error — retry\r\n        }\r\n    }\r\n    if existed_unparseable {\r\n        RawRead::Corrupt\r\n    } else {\r\n        RawRead::Absent\r\n    }\r\n}\r\n\r\n/// [`read_raw_state`] collapsed to an `Option` for callers that treat absent and\r\n/// corrupt identically (status/unbound lookups fall through to the external path\r\n/// for either). Liveness-deciding callers use [`read_raw_state`] directly so a\r\n/// corrupt record is not mistaken for a live one.\r\nfn read_raw(perch_path: &Path) -> Option<Value> {\r\n    match read_raw_state(perch_path) {\r\n        RawRead::Present(v) => Some(v),\r\n        RawRead::Absent | RawRead::Corrupt => None,\r\n    }\r\n}\r\n\r\n/// The daemon-managed `status` of a perch, if it is daemon-hosted. `Some(_)` ⇒\r\n/// daemon-hosted (liveness is authoritative via status); `None` ⇒ external /\r\n/// interim (liveness is the per-pid probe).\r\nfn read_status(perch_path: &Path) -> Option<String> {\r\n    read_raw(perch_path)?\r\n        .get(\"status\")\r\n        .and_then(|v| v.as_str())\r\n        .map(str::to_string)\r\n}\r\n\r\n/// True when a perch is in the `unbound` state (skeleton created, harness not\r\n/// yet called `spt api bind`). The endpoint is broker-attachable but NOT\r\n/// message-addressable — `is_perch_alive` stays `false` for it.\r\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\npub fn is_perch_unbound(perch_path: &Path) -> bool {\r\n    read_status(perch_path).as_deref() == Some(STATUS_UNBOUND)\r\n}\r\n\r\n/// Liveness for a perch by its **directory**. THE resolver for the path-aware\r\n/// call sites (`is_online`, `from_has_live_perch`, roster).\r\n///\r\n/// - **Daemon-hosted** (`status` present): live iff `status == \"online\"`. The\r\n///   `info.pid` (an ephemeral summarizer pid, or the shared daemon pid) is\r\n///   **never** consulted — killing the summarizer does not flip the perch.\r\n/// - **External/interim** (`status` absent): the per-pid probe, preserving the\r\n///   exact interim semantics — a numeric pid is probed; a `\"BUSY\"` sentinel or\r\n///   unparseable/absent pid reads alive (the listener is up between polls).\r\n/// - **Corrupt** (file present, persistently unparseable): NOT alive\r\n///   (REQ-HAZARD-CORRUPT-PERCH-COHERENCE) — a destroyed record (e.g. a hard-reset\r\n///   NUL zero-fill) must never read as a live endpoint. This is the ONE departure\r\n///   from interim parity: ABSENT stays alive, CORRUPT does not.\r\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n// [impl->REQ-HAZARD-CORRUPT-PERCH-COHERENCE]\r\npub fn is_perch_alive(perch_path: &Path) -> bool {\r\n    match read_raw_state(perch_path) {\r\n        RawRead::Present(val) => {\r\n            if let Some(status) = val.get(\"status\").and_then(|v| v.as_str()) {\r\n                return status == STATUS_ONLINE; // daemon-authoritative; never pid\r\n            }\r\n            match val.get(\"pid\").and_then(|v| v.as_u64()) {\r\n                Some(pid) => is_process_alive(pid as u32),\r\n                None => true, // BUSY sentinel / unparseable pid — interim parity\r\n            }\r\n        }\r\n        RawRead::Absent => true, // absent info.json — interim parity (read_pid None ⇒ alive)\r\n        RawRead::Corrupt => false, // destroyed record is NOT a live endpoint\r\n    }\r\n}\r\n\r\n/// Liveness for a **registry row**: its id plus the pid it was registered under,\r\n/// resolved within `owlery` (the registry's own root). Daemon-hosted rows key on\r\n/// the endpoint `status`; external rows pid-probe the **row's** pid exactly as\r\n/// the interim `clean_stale_entries` did — so the daemon swap touches only the\r\n/// hosted rows. An offline daemon-hosted endpoint is cleaned even though the\r\n/// (shared) daemon pid is still alive; a still-online one survives a dead\r\n/// ephemeral pid.\r\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\npub fn is_registry_entry_alive(id: &str, registry_pid: u32, owlery: &Path) -> bool {\r\n    let perch_path = perch::resolve_perch_path_in(owlery, id, ParentHint::Infer);\r\n    match read_status(&perch_path) {\r\n        // `unbound` is a live skeleton — don't clean it; `online` is live; anything\r\n        // else (offline, unknown) is stale → clean.\r\n        Some(status) => status == STATUS_ONLINE || status == STATUS_UNBOUND,\r\n        None => is_process_alive(registry_pid), // external/interim: pid-probe the row\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::info::{set_status, write_info, InfoJson};\r\n\r\n    /// A pid that is never a real allocation (above Linux pid_max ~4.2M and\r\n    /// Windows ranges; positive as i32). Deterministic — avoids pid-reuse flake.\r\n    const DEAD_PID: u32 = 2_000_000_000;\r\n\r\n    fn perch() -> tempfile::TempDir {\r\n        tempfile::tempdir().unwrap()\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked\r\n    // `online` reads alive even though its recorded (summarizer) pid is DEAD —\r\n    // killing the summarizer subprocess does not flip the perch offline.\r\n    #[test]\r\n    fn daemon_hosted_online_survives_dead_pid() {\r\n        let d = perch();\r\n        // Record the ephemeral summarizer pid (now dead) then mark daemon-online.\r\n        write_info(\r\n            d.path(),\r\n            &InfoJson::new(\"doyle-psyche\", \"t\", DEAD_PID, \"sid\", \"psyche\"),\r\n        )\r\n        .unwrap();\r\n        set_status(d.path(), STATUS_ONLINE).unwrap();\r\n        assert!(\r\n            is_perch_alive(d.path()),\r\n            \"online status is authoritative; the dead pid is ignored\"\r\n        );\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked\r\n    // `offline` reads dead even though its recorded pid is ALIVE (this process).\r\n    #[test]\r\n    fn daemon_hosted_offline_overrides_live_pid() {\r\n        let d = perch();\r\n        write_info(\r\n            d.path(),\r\n            &InfoJson::new(\"doyle-psyche\", \"t\", std::process::id(), \"sid\", \"psyche\"),\r\n        )\r\n        .unwrap();\r\n        set_status(d.path(), STATUS_OFFLINE).unwrap();\r","truncated":true,"truncatedBy":"lines","totalLines":392,"totalBytes":10534,"outputLines":213,"outputBytes":10534,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! **The** single liveness resolver — daemon-authoritative for daemon-hosted\r\n//! perches, per-pid for everything else (KNOWN-HAZARDS 2.5,\r\n//! REQ-HAZARD-DAEMON-HOSTED-LIVENESS).\r\n//!\r\n//! ## Why one resolver\r\n//! The sister project, and the spt-core **interim** (M1/M2a/M2b) model, evaluate\r\n//! a perch's liveness from its owning process pid: `is_process_alive(info.pid)`,\r\n//! with `clean_stale_entries` dropping dead-pid registry rows. That is correct\r\n//! while every perch has a *dedicated* per-process owner.\r\n//!\r\n//! Under ADR-0004 the daemon hosts the Psyche (and any spt-hosted Self) as a\r\n//! **loop inside the daemon**, not a separate process. Such a perch holds no\r\n//! dedicated pid; its `claude`/summarizer subprocess is **ephemeral** (spawned\r\n//! per pulse/commune, then exits). If liveness keyed on that pid, killing the\r\n//! summarizer would read the whole perch \"offline\"; if it keyed on the daemon's\r\n//! pid, every hosted endpoint would share one pid and `clean_stale_entries`\r\n//! could never distinguish a torn-down endpoint from a live one (2.5).\r\n//!\r\n//! So for **daemon-hosted** perches liveness is the daemon-managed `status` field\r\n//! on `info.json` ([`crate::info::set_status`]); for **external/interim** perches\r\n//! it stays the per-pid probe. Both decisions live **here**, behind one resolver\r\n//! (mirroring how [`crate::registry::resolve_address`] centralizes stale-clean),\r\n//! so the daemon swap is localized and the per-pid assumption never leaks into a\r\n//! new call site. The three call sites — `deliver::is_online`,\r\n//! `ring::from_has_live_perch`, `roster::enumerate` (perch path) and\r\n//! `registry::clean_stale_entries` (registry row) — all route through this module.\r\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n\r\nuse std::path::Path;\r\n\r\nuse serde_json::Value;\r\n\r\nuse crate::perch::{self, ParentHint};\r\nuse crate::proc::is_process_alive;\r\n\r\n/// Daemon-managed `status` value meaning the hosted endpoint is live.\r\npub const STATUS_ONLINE: &str = \"online\";\r\n/// Daemon-managed `status` value meaning the hosted endpoint is torn down.\r\npub const STATUS_OFFLINE: &str = \"offline\";\r\n/// Daemon-managed `status` value: a skeleton perch was created (harness spawned)\r\n/// but `spt api bind` has not yet been called. The endpoint is broker-attachable\r\n/// (a live PTY session exists) but NOT message-addressable (no `session_id` yet).\r\n/// Lifecycle: spawn/skeleton-create → `unbound`; bind → `online`; session death\r\n/// → `offline` via the B2 reconcile.\r\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\npub const STATUS_UNBOUND: &str = \"unbound\";\r\n\r\n/// Torn-read retry budget (mirrors [`crate::info`]): a reader may hit the file\r\n/// mid-replace; retry a few times, then degrade rather than panic.\r\nconst READ_ATTEMPTS: u32 = 5;\r\n\r\n/// The outcome of a tolerant `info.json` read, distinguishing the two failure\r\n/// modes a liveness decision must NOT conflate (REQ-HAZARD-CORRUPT-PERCH-COHERENCE):\r\n/// - [`Present`](RawRead::Present): parsed a value (possibly schema-minimal).\r\n/// - [`Absent`](RawRead::Absent): no file — the perch dir carries no record (or the\r\n///   read never got past a non-existence error). Interim parity treats this as alive.\r\n/// - [`Corrupt`](RawRead::Corrupt): the file EXISTS but stays unparseable across every\r\n///   retry — a destroyed record (e.g. a hard-reset NUL zero-fill). NOT alive: a wiped\r\n///   record must never read as a live endpoint.\r\nenum RawRead {\r\n    Present(Value),\r\n    Absent,\r\n    Corrupt,\r\n}\r\n\r\n/// One tolerant read of a perch's `info.json` as a raw value — schema-minimal\r\n/// records (`{\"pid\": N}` from wrapper-owned listeners) parse where the full\r\n/// [`crate::info::InfoJson`] shape would not. Distinguishes ABSENT (no file) from\r\n/// CORRUPT (file present, persistently unparseable) — a mid-replace torn read is\r\n/// absorbed by the retry loop (the atomic rename means a retry sees the complete\r\n/// old or new file), so only a genuinely destroyed record degrades to `Corrupt`.\r\nfn read_raw_state(perch_path: &Path) -> RawRead {\r\n    let path = perch_path.join(\"info.json\");\r\n    let mut existed_unparseable = false;\r\n    for _ in 0..READ_ATTEMPTS {\r\n        match std::fs::read_to_string(&path) {\r\n            Ok(content) => match serde_json::from_str::<Value>(&content) {\r\n                Ok(val) => return RawRead::Present(val),\r\n                Err(_) => existed_unparseable = true, // present but bad — retry (mid-replace?)\r\n            },\r\n            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return RawRead::Absent,\r\n            Err(_) => {} // transient sharing/access error — retry\r\n        }\r\n    }\r\n    if existed_unparseable {\r\n        RawRead::Corrupt\r\n    } else {\r\n        RawRead::Absent\r\n    }\r\n}\r\n\r\n/// [`read_raw_state`] collapsed to an `Option` for callers that treat absent and\r\n/// corrupt identically (status/unbound lookups fall through to the external path\r\n/// for either). Liveness-deciding callers use [`read_raw_state`] directly so a\r\n/// corrupt record is not mistaken for a live one.\r\nfn read_raw(perch_path: &Path) -> Option<Value> {\r\n    match read_raw_state(perch_path) {\r\n        RawRead::Present(v) => Some(v),\r\n        RawRead::Absent | RawRead::Corrupt => None,\r\n    }\r\n}\r\n\r\n/// The daemon-managed `status` of a perch, if it is daemon-hosted. `Some(_)` ⇒\r\n/// daemon-hosted (liveness is authoritative via status); `None` ⇒ external /\r\n/// interim (liveness is the per-pid probe).\r\nfn read_status(perch_path: &Path) -> Option<String> {\r\n    read_raw(perch_path)?\r\n        .get(\"status\")\r\n        .and_then(|v| v.as_str())\r\n        .map(str::to_string)\r\n}\r\n\r\n/// True when a perch is in the `unbound` state (skeleton created, harness not\r\n/// yet called `spt api bind`). The endpoint is broker-attachable but NOT\r\n/// message-addressable — `is_perch_alive` stays `false` for it.\r\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\npub fn is_perch_unbound(perch_path: &Path) -> bool {\r\n    read_status(perch_path).as_deref() == Some(STATUS_UNBOUND)\r\n}\r\n\r\n/// Liveness for a perch by its **directory**. THE resolver for the path-aware\r\n/// call sites (`is_online`, `from_has_live_perch`, roster).\r\n///\r\n/// - **Daemon-hosted** (`status` present): live iff `status == \"online\"`. The\r\n///   `info.pid` (an ephemeral summarizer pid, or the shared daemon pid) is\r\n///   **never** consulted — killing the summarizer does not flip the perch.\r\n/// - **External/interim** (`status` absent): the per-pid probe, preserving the\r\n///   exact interim semantics — a numeric pid is probed; a `\"BUSY\"` sentinel or\r\n///   unparseable/absent pid reads alive (the listener is up between polls).\r\n/// - **Corrupt** (file present, persistently unparseable): NOT alive\r\n///   (REQ-HAZARD-CORRUPT-PERCH-COHERENCE) — a destroyed record (e.g. a hard-reset\r\n///   NUL zero-fill) must never read as a live endpoint. This is the ONE departure\r\n///   from interim parity: ABSENT stays alive, CORRUPT does not.\r\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n// [impl->REQ-HAZARD-CORRUPT-PERCH-COHERENCE]\r\npub fn is_perch_alive(perch_path: &Path) -> bool {\r\n    match read_raw_state(perch_path) {\r\n        RawRead::Present(val) => {\r\n            if let Some(status) = val.get(\"status\").and_then(|v| v.as_str()) {\r\n                return status == STATUS_ONLINE; // daemon-authoritative; never pid\r\n            }\r\n            match val.get(\"pid\").and_then(|v| v.as_u64()) {\r\n                Some(pid) => is_process_alive(pid as u32),\r\n                None => true, // BUSY sentinel / unparseable pid — interim parity\r\n            }\r\n        }\r\n        RawRead::Absent => true, // absent info.json — interim parity (read_pid None ⇒ alive)\r\n        RawRead::Corrupt => false, // destroyed record is NOT a live endpoint\r\n    }\r\n}\r\n\r\n/// Liveness for a **registry row**: its id plus the pid it was registered under,\r\n/// resolved within `owlery` (the registry's own root). Daemon-hosted rows key on\r\n/// the endpoint `status`; external rows pid-probe the **row's** pid exactly as\r\n/// the interim `clean_stale_entries` did — so the daemon swap touches only the\r\n/// hosted rows. An offline daemon-hosted endpoint is cleaned even though the\r\n/// (shared) daemon pid is still alive; a still-online one survives a dead\r\n/// ephemeral pid.\r\n// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]\r\n// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]\r\npub fn is_registry_entry_alive(id: &str, registry_pid: u32, owlery: &Path) -> bool {\r\n    let perch_path = perch::resolve_perch_path_in(owlery, id, ParentHint::Infer);\r\n    match read_status(&perch_path) {\r\n        // `unbound` is a live skeleton — don't clean it; `online` is live; anything\r\n        // else (offline, unknown) is stale → clean.\r\n        Some(status) => status == STATUS_ONLINE || status == STATUS_UNBOUND,\r\n        None => is_process_alive(registry_pid), // external/interim: pid-probe the row\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n    use crate::info::{set_status, write_info, InfoJson};\r\n\r\n    /// A pid that is never a real allocation (above Linux pid_max ~4.2M and\r\n    /// Windows ranges; positive as i32). Deterministic — avoids pid-reuse flake.\r\n    const DEAD_PID: u32 = 2_000_000_000;\r\n\r\n    fn perch() -> tempfile::TempDir {\r\n        tempfile::tempdir().unwrap()\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked\r\n    // `online` reads alive even though its recorded (summarizer) pid is DEAD —\r\n    // killing the summarizer subprocess does not flip the perch offline.\r\n    #[test]\r\n    fn daemon_hosted_online_survives_dead_pid() {\r\n        let d = perch();\r\n        // Record the ephemeral summarizer pid (now dead) then mark daemon-online.\r\n        write_info(\r\n            d.path(),\r\n            &InfoJson::new(\"doyle-psyche\", \"t\", DEAD_PID, \"sid\", \"psyche\"),\r\n        )\r\n        .unwrap();\r\n        set_status(d.path(), STATUS_ONLINE).unwrap();\r\n        assert!(\r\n            is_perch_alive(d.path()),\r\n            \"online status is authoritative; the dead pid is ignored\"\r\n        );\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked\r\n    // `offline` reads dead even though its recorded pid is ALIVE (this process).\r\n    #[test]\r\n    fn daemon_hosted_offline_overrides_live_pid() {\r\n        let d = perch();\r\n        write_info(\r\n            d.path(),\r\n            &InfoJson::new(\"doyle-psyche\", \"t\", std::process::id(), \"sid\", \"psyche\"),\r\n        )\r\n        .unwrap();\r\n        set_status(d.path(), STATUS_OFFLINE).unwrap();\r\n…\n    }\n…\n}","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,null,218,null,391]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-store\\src\\liveness.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":392,"totalBytes":10534,"outputLines":213,"outputBytes":10534,"shownRange":{"start":1,"end":213},"nextOffset":214}}},"isError":false,"timestamp":1784197487232}}
{"type":"message","id":"2030928b","parentId":"3709ebcd","timestamp":"2026-07-16T10:24:47.249Z","message":{"role":"toolResult","toolCallId":"call_S6QdmFKNSNnotwZ8ZejJNokO|fc_017bff223290b7ba016a58b16f327c819a8a2312703f4d584d","toolName":"read","content":[{"type":"text","text":"pub fn wan_send(target: &str, from: &str, body: &str) -> WanSendOutcome {\n    let own_hex = match spt_store::nodeid::load_or_create() {\n        Ok(id) => id.public_key().to_hex(),\n        Err(e) => return WanSendOutcome::Failed(format!(\"node identity: {e}\")),\n    };\n    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\n    if spt_daemon::daemon::ensure_running().is_err() {\n        return WanSendOutcome::Failed(\"daemon did not come up\".to_string());\n    }\n    let mut brain = match Brain::cold_start(&broker_socket_name(), now_ms()) {\n        Ok(b) => b,\n        Err(e) => return WanSendOutcome::Failed(format!(\"broker connect: {e}\")),\n    };\n    let mut ops = EpochSource::load_from(&perch::identity_dir().join(\"cli-ops.json\"));\n    let op_id = match ops.next_epoch() {\n        Ok(e) => format!(\"{own_hex}:cli:{e}\"),\n        Err(e) => return WanSendOutcome::Failed(format!(\"op counter: {e}\")),\n    };\n    wan_send_with(\n        &mut brain,\n        &regs,\n        &own_hex,\n        target,\n        from,\n        body,\n        &op_id,\n        seed_first_addr,\n    )\n}\n\n/// The testable core: resolve against `regs`, dial via `resolver`, write one\n/// `WanMessage` line, finish the stream.\n#[allow(clippy::too_many_arguments)]\npub fn wan_send_with(\n    brain: &mut Brain,\n    regs: &BTreeMap<String, SubnetRegistry>,\n    own_hex: &str,\n    target: &str,\n    from: &str,\n    body: &str,\n    op_id: &str,\n    resolver: impl Fn(&str) -> Option<serde_json::Value>,\n) -> WanSendOutcome {\n    let address = match Address::parse(target) {\n        Ok(a) => a,\n        Err(e) => return WanSendOutcome::Failed(format!(\"bad address: {e:?}\")),\n    };\n\n    // The same exclusion rule the registry's advertisement side enforces\n    // (ADR-0006 §6): hidden = not routable, even via an explicit pin.\n    let subnets = SubnetStore::load();\n    let vis = VisibilityStore::load();\n    let hide_new: BTreeMap<String, bool> = subnets\n        .subnets\n        .iter()\n        .map(|s| (s.name.clone(), s.hide_new_endpoints))\n        .collect();\n    let excluded =\n        |subnet: &str, id: &str| vis.hidden(id, subnet, *hide_new.get(subnet).unwrap_or(&false));\n\n    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {\n        Resolution::Resolved { instance, .. } => instance,\n        // Refusals render as valid copy-paste targets (REQ-INST-10), never a\n        // debug dump the user can't act on.\n        Resolution::Ambiguous(a) => {\n            return WanSendOutcome::Ambiguous(render_refusal(&address.id, &a, regs))\n        }\n        Resolution::NotFound => return WanSendOutcome::NotFound,\n    };\n    if instance.node == own_hex {\n        // A local instance resolves here only when the local perch scan\n        // missed it (registry staleness) — WAN-dialing ourselves helps\n        // nobody; let the caller report the local outcome.\n        return WanSendOutcome::NotFound;\n    }\n\n    let Some(addr) = resolver(&instance.node) else {\n        return WanSendOutcome::Failed(format!(\"no dialable address for {}\", instance.node));\n    };\n    let msg = WanMessage {\n        target: address.id.clone(),\n        from: from.to_string(),\n        body: body.to_string(),\n        op_id: op_id.to_string(),\n    };\n    // #9/#10 (REQ-WAN-SEND-DELIVERY): dial, then round-trip — send the message,\n    // finish our side, and BLOCK for the receiver's reply under the broker read\n    // deadline. The reply is what makes SENT(WAN) honest; the old code sent\n    // fire-and-forget (finish=true, no read) and returned Sent the instant the\n    // bytes left the local broker, even when the payload never reached a peer.\n    let conn = match brain.net_dial(addr, None) {\n        Ok(c) => c,\n        Err(e) => return WanSendOutcome::Failed(e.to_string()),\n    };\n    let node = instance.node;\n    match spt_daemon::request_wan(brain, conn.conn_id, &msg) {\n        Ok(outcome) => classify_wan_reply(outcome, node),\n        Err(e) => WanSendOutcome::Failed(e.to_string()),\n    }\n}\n\n/// Map the receiver's confirmed [`WanRequestOutcome`] to the CLI's honest\n/// [`WanSendOutcome`] (#9/#10). The delivery-positive outcomes\n/// (Delivered/Spooled/Duplicate) become `Sent` — the ONLY ones that print\n/// SENT(WAN); Refused/NoPerch/NoReply each surface as their own honest\n/// non-delivery. Pure so the honest-SENT decision is unit-tested against a seam\n/// outcome (no live cross-host round-trip needed — that is the [twohost] int).\n// [impl->REQ-WAN-SEND-DELIVERY]\npub fn classify_wan_reply(outcome: spt_daemon::WanRequestOutcome, node: String) -> WanSendOutcome {\n    use spt_daemon::WanRequestOutcome as O;\n    match outcome {\n        O::Delivered => WanSendOutcome::Sent {\n            node,\n            how: \"delivered\",\n        },\n        O::Spooled => WanSendOutcome::Sent {\n            node,\n…\n        },\n…\n    }\n}\n\n[Showing lines 179-293 of 1429. Use :294 to continue]"}],"details":{"truncation":{"content":"        Ok(id) => id.public_key().to_hex(),\r\n        Err(e) => return WanSendOutcome::Failed(format!(\"node identity: {e}\")),\r\n    };\r\n    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\r\n    if spt_daemon::daemon::ensure_running().is_err() {\r\n        return WanSendOutcome::Failed(\"daemon did not come up\".to_string());\r\n    }\r\n    let mut brain = match Brain::cold_start(&broker_socket_name(), now_ms()) {\r\n        Ok(b) => b,\r\n        Err(e) => return WanSendOutcome::Failed(format!(\"broker connect: {e}\")),\r\n    };\r\n    let mut ops = EpochSource::load_from(&perch::identity_dir().join(\"cli-ops.json\"));\r\n    let op_id = match ops.next_epoch() {\r\n        Ok(e) => format!(\"{own_hex}:cli:{e}\"),\r\n        Err(e) => return WanSendOutcome::Failed(format!(\"op counter: {e}\")),\r\n    };\r\n    wan_send_with(\r\n        &mut brain,\r\n        &regs,\r\n        &own_hex,\r\n        target,\r\n        from,\r\n        body,\r\n        &op_id,\r\n        seed_first_addr,\r\n    )\r\n}\r\n\r\n/// The testable core: resolve against `regs`, dial via `resolver`, write one\r\n/// `WanMessage` line, finish the stream.\r\n#[allow(clippy::too_many_arguments)]\r\npub fn wan_send_with(\r\n    brain: &mut Brain,\r\n    regs: &BTreeMap<String, SubnetRegistry>,\r\n    own_hex: &str,\r\n    target: &str,\r\n    from: &str,\r\n    body: &str,\r\n    op_id: &str,\r\n    resolver: impl Fn(&str) -> Option<serde_json::Value>,\r\n) -> WanSendOutcome {\r\n    let address = match Address::parse(target) {\r\n        Ok(a) => a,\r\n        Err(e) => return WanSendOutcome::Failed(format!(\"bad address: {e:?}\")),\r\n    };\r\n\r\n    // The same exclusion rule the registry's advertisement side enforces\r\n    // (ADR-0006 §6): hidden = not routable, even via an explicit pin.\r\n    let subnets = SubnetStore::load();\r\n    let vis = VisibilityStore::load();\r\n    let hide_new: BTreeMap<String, bool> = subnets\r\n        .subnets\r\n        .iter()\r\n        .map(|s| (s.name.clone(), s.hide_new_endpoints))\r\n        .collect();\r\n    let excluded =\r\n        |subnet: &str, id: &str| vis.hidden(id, subnet, *hide_new.get(subnet).unwrap_or(&false));\r\n\r\n    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {\r\n        Resolution::Resolved { instance, .. } => instance,\r\n        // Refusals render as valid copy-paste targets (REQ-INST-10), never a\r\n        // debug dump the user can't act on.\r\n        Resolution::Ambiguous(a) => {\r\n            return WanSendOutcome::Ambiguous(render_refusal(&address.id, &a, regs))\r\n        }\r\n        Resolution::NotFound => return WanSendOutcome::NotFound,\r\n    };\r\n    if instance.node == own_hex {\r\n        // A local instance resolves here only when the local perch scan\r\n        // missed it (registry staleness) — WAN-dialing ourselves helps\r\n        // nobody; let the caller report the local outcome.\r\n        return WanSendOutcome::NotFound;\r\n    }\r\n\r\n    let Some(addr) = resolver(&instance.node) else {\r\n        return WanSendOutcome::Failed(format!(\"no dialable address for {}\", instance.node));\r\n    };\r\n    let msg = WanMessage {\r\n        target: address.id.clone(),\r\n        from: from.to_string(),\r\n        body: body.to_string(),\r\n        op_id: op_id.to_string(),\r\n    };\r\n    // #9/#10 (REQ-WAN-SEND-DELIVERY): dial, then round-trip — send the message,\r\n    // finish our side, and BLOCK for the receiver's reply under the broker read\r\n    // deadline. The reply is what makes SENT(WAN) honest; the old code sent\r\n    // fire-and-forget (finish=true, no read) and returned Sent the instant the\r\n    // bytes left the local broker, even when the payload never reached a peer.\r\n    let conn = match brain.net_dial(addr, None) {\r\n        Ok(c) => c,\r\n        Err(e) => return WanSendOutcome::Failed(e.to_string()),\r\n    };\r\n    let node = instance.node;\r\n    match spt_daemon::request_wan(brain, conn.conn_id, &msg) {\r\n        Ok(outcome) => classify_wan_reply(outcome, node),\r\n        Err(e) => WanSendOutcome::Failed(e.to_string()),\r\n    }\r\n}\r\n\r\n/// Map the receiver's confirmed [`WanRequestOutcome`] to the CLI's honest\r\n/// [`WanSendOutcome`] (#9/#10). The delivery-positive outcomes\r\n/// (Delivered/Spooled/Duplicate) become `Sent` — the ONLY ones that print\r\n/// SENT(WAN); Refused/NoPerch/NoReply each surface as their own honest\r\n/// non-delivery. Pure so the honest-SENT decision is unit-tested against a seam\r\n/// outcome (no live cross-host round-trip needed — that is the [twohost] int).\r\n// [impl->REQ-WAN-SEND-DELIVERY]\r\npub fn classify_wan_reply(outcome: spt_daemon::WanRequestOutcome, node: String) -> WanSendOutcome {\r\n    use spt_daemon::WanRequestOutcome as O;\r\n    match outcome {\r\n        O::Delivered => WanSendOutcome::Sent {\r\n            node,\r\n            how: \"delivered\",\r\n        },\r\n        O::Spooled => WanSendOutcome::Sent {\r\n            node,\r","truncated":true,"truncatedBy":"lines","totalLines":1251,"totalBytes":4797,"outputLines":115,"outputBytes":4797,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"pub fn wan_send(target: &str, from: &str, body: &str) -> WanSendOutcome {\n    let own_hex = match spt_store::nodeid::load_or_create() {\n        Ok(id) => id.public_key().to_hex(),\r\n        Err(e) => return WanSendOutcome::Failed(format!(\"node identity: {e}\")),\r\n    };\r\n    let regs = load_snapshots(&perch::identity_dir().join(\"registry\"));\r\n    if spt_daemon::daemon::ensure_running().is_err() {\r\n        return WanSendOutcome::Failed(\"daemon did not come up\".to_string());\r\n    }\r\n    let mut brain = match Brain::cold_start(&broker_socket_name(), now_ms()) {\r\n        Ok(b) => b,\r\n        Err(e) => return WanSendOutcome::Failed(format!(\"broker connect: {e}\")),\r\n    };\r\n    let mut ops = EpochSource::load_from(&perch::identity_dir().join(\"cli-ops.json\"));\r\n    let op_id = match ops.next_epoch() {\r\n        Ok(e) => format!(\"{own_hex}:cli:{e}\"),\r\n        Err(e) => return WanSendOutcome::Failed(format!(\"op counter: {e}\")),\r\n    };\r\n    wan_send_with(\r\n        &mut brain,\r\n        &regs,\r\n        &own_hex,\r\n        target,\r\n        from,\r\n        body,\r\n        &op_id,\r\n        seed_first_addr,\r\n    )\r\n}\r\n\r\n/// The testable core: resolve against `regs`, dial via `resolver`, write one\r\n/// `WanMessage` line, finish the stream.\r\n#[allow(clippy::too_many_arguments)]\r\npub fn wan_send_with(\r\n    brain: &mut Brain,\r\n    regs: &BTreeMap<String, SubnetRegistry>,\r\n    own_hex: &str,\r\n    target: &str,\r\n    from: &str,\r\n    body: &str,\r\n    op_id: &str,\r\n    resolver: impl Fn(&str) -> Option<serde_json::Value>,\r\n) -> WanSendOutcome {\r\n    let address = match Address::parse(target) {\r\n        Ok(a) => a,\r\n        Err(e) => return WanSendOutcome::Failed(format!(\"bad address: {e:?}\")),\r\n    };\r\n\r\n    // The same exclusion rule the registry's advertisement side enforces\r\n    // (ADR-0006 §6): hidden = not routable, even via an explicit pin.\r\n    let subnets = SubnetStore::load();\r\n    let vis = VisibilityStore::load();\r\n    let hide_new: BTreeMap<String, bool> = subnets\r\n        .subnets\r\n        .iter()\r\n        .map(|s| (s.name.clone(), s.hide_new_endpoints))\r\n        .collect();\r\n    let excluded =\r\n        |subnet: &str, id: &str| vis.hidden(id, subnet, *hide_new.get(subnet).unwrap_or(&false));\r\n\r\n    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {\r\n        Resolution::Resolved { instance, .. } => instance,\r\n        // Refusals render as valid copy-paste targets (REQ-INST-10), never a\r\n        // debug dump the user can't act on.\r\n        Resolution::Ambiguous(a) => {\r\n            return WanSendOutcome::Ambiguous(render_refusal(&address.id, &a, regs))\r\n        }\r\n        Resolution::NotFound => return WanSendOutcome::NotFound,\r\n    };\r\n    if instance.node == own_hex {\r\n        // A local instance resolves here only when the local perch scan\r\n        // missed it (registry staleness) — WAN-dialing ourselves helps\r\n        // nobody; let the caller report the local outcome.\r\n        return WanSendOutcome::NotFound;\r\n    }\r\n\r\n    let Some(addr) = resolver(&instance.node) else {\r\n        return WanSendOutcome::Failed(format!(\"no dialable address for {}\", instance.node));\r\n    };\r\n    let msg = WanMessage {\r\n        target: address.id.clone(),\r\n        from: from.to_string(),\r\n        body: body.to_string(),\r\n        op_id: op_id.to_string(),\r\n    };\r\n    // #9/#10 (REQ-WAN-SEND-DELIVERY): dial, then round-trip — send the message,\r\n    // finish our side, and BLOCK for the receiver's reply under the broker read\r\n    // deadline. The reply is what makes SENT(WAN) honest; the old code sent\r\n    // fire-and-forget (finish=true, no read) and returned Sent the instant the\r\n    // bytes left the local broker, even when the payload never reached a peer.\r\n    let conn = match brain.net_dial(addr, None) {\r\n        Ok(c) => c,\r\n        Err(e) => return WanSendOutcome::Failed(e.to_string()),\r\n    };\r\n    let node = instance.node;\r\n    match spt_daemon::request_wan(brain, conn.conn_id, &msg) {\r\n        Ok(outcome) => classify_wan_reply(outcome, node),\r\n        Err(e) => WanSendOutcome::Failed(e.to_string()),\r\n    }\r\n}\r\n\r\n/// Map the receiver's confirmed [`WanRequestOutcome`] to the CLI's honest\r\n/// [`WanSendOutcome`] (#9/#10). The delivery-positive outcomes\r\n/// (Delivered/Spooled/Duplicate) become `Sent` — the ONLY ones that print\r\n/// SENT(WAN); Refused/NoPerch/NoReply each surface as their own honest\r\n/// non-delivery. Pure so the honest-SENT decision is unit-tested against a seam\r\n/// outcome (no live cross-host round-trip needed — that is the [twohost] int).\r\n// [impl->REQ-WAN-SEND-DELIVERY]\r\npub fn classify_wan_reply(outcome: spt_daemon::WanRequestOutcome, node: String) -> WanSendOutcome {\r\n    use spt_daemon::WanRequestOutcome as O;\r\n    match outcome {\r\n        O::Delivered => WanSendOutcome::Sent {\r\n            node,\r\n            how: \"delivered\",\r\n        },\r\n        O::Spooled => WanSendOutcome::Sent {\r\n            node,\r\n…\n        },\n…\n    }\n}","startLine":177,"lineNumbers":[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,null,295,null,303,304]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\wansend.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":1429,"totalBytes":4797,"outputLines":115,"outputBytes":4797,"shownRange":{"start":179,"end":293},"nextOffset":294}}},"isError":false,"timestamp":1784197487249}}
{"type":"message","id":"636f0775","parentId":"2030928b","timestamp":"2026-07-16T10:24:47.251Z","message":{"role":"toolResult","toolCallId":"call_bAklXmShNsKVbGZZom2flVX1|fc_017bff223290b7ba016a58b16f328c819a8051514240668dbc","toolName":"grep","content":[{"type":"text","text":"# crates/spt/\n\n## src/\n### cli.rs\n 70|    /// Send a message (body read from stdin); fire-and-forget.\n*71|    Send {\n 72|        // [impl->REQ-SEND-REPLYTO-REMOVE] `--reply-to` is hard-removed; `target` is\n 73|        // a required positional (no reply-to fallback). Reply-correlation rides the\n 74|        // structural `from`, not a flag.\n 75|        /// Target perch id.\n*76|        target: String,\n 77|        /// Sender id carried structurally as the message `from` (auto-detected from session if omitted).\n 78|        #[arg(long)]\n 79|        from: Option<String>,\n...\n 119|        /// Target perch id.\n*120|        target: String,\n 121|        /// Sender id (auto-detected from session if omitted).\n 122|        #[arg(long)]\n 123|        from: Option<String>,\n...\n 819|        /// `<adapter>` or host `<binary>` whose pointer to drop.\n*820|        target: String,\n 821|        /// Clear the pointer for `target` instead of setting it.\n 822|        #[arg(long)]\n 823|        clear: bool,\n...\n 911|        /// The capability op + args (vocabulary-checked against the manifest).\n*912|        #[arg(trailing_var_arg = true)]\n 913|        op: Vec<String>,\n 914|        #[arg(long)]\n 915|        owner: Option<String>,\n...\n 950|    /// File transfers are progress-queryable by xfer id.\n*951|    Send {\n 952|        /// Canonical id or alias.\n 953|        shell_ref: String,\n 954|        /// The text payload.\n...\n 1157|        #[arg(long)]\n*1158|        target: Option<String>,\n 1159|        /// Issuer endpoint id (auto-detected from session if omitted).\n 1160|        #[arg(long)]\n 1161|        from: Option<String>,\n...\n 1289|    match command {\n*1290|        Cmd::Send {\n 1291|            target,\n 1292|            from,\n 1293|            idle_only,\n...\n 1411|                    // resolves it (resume info.cwd → current_dir).\n*1412|                    RunTarget::Direct { adapter, id } => cmd_endpoint_run(\n 1413|                        &adapter, &id, resume, None, start, attach, view, subnet.as_deref(), save,\n 1414|                    ),\n 1415|                    // Bare (or partial) → the interactive picker (REQ-RUN-PICKER).\n...\n 1417|                    // perch pre-fills the create-new id.\n*1418|                    RunTarget::Picker { adapter, id } => crate::picker::run(adapter, id),\n 1419|                }\n 1420|            }\n 1421|        },\n...\n 1822|    match (adapter, id) {\n*1823|        (Some(adapter), Some(id)) => RunTarget::Direct { adapter, id },\n 1824|        (None, Some(id)) => match recorded(&id) {\n*1825|            Some(adapter) => RunTarget::Direct { adapter, id },\n*1826|            None => RunTarget::Picker { adapter: None, id: Some(id) },\n 1827|        },\n*1828|        (adapter, id) => RunTarget::Picker { adapter, id },\n 1829|    }\n 1830|}\n 1831|\n...\n 1851|            resolve_run_target(s(\"claude-spt\"), s(\"foo\"), |_| panic!(\"must not read perch\")),\n*1852|            RunTarget::Direct { adapter: \"claude-spt\".into(), id: \"foo\".into() }\n 1853|        );\n 1854|        // (None,Some) + existing perch's recorded adapter → Direct (the D-1 reuse).\n 1855|        assert_eq!(\n...\n 1859|            }),\n*1860|            RunTarget::Direct { adapter: \"claude-spt\".into(), id: \"foo\".into() }\n 1861|        );\n 1862|        // (None,Some) + no perch / adapterless → today's create-new picker prefill.\n 1863|        assert_eq!(\n 1864|            resolve_run_target(None, s(\"foo\"), |_| None),\n*1865|            RunTarget::Picker { adapter: None, id: s(\"foo\") }\n 1866|        );\n 1867|        // (Some,None) → picker with the adapter pre-selected.\n 1868|        assert_eq!(\n 1869|            resolve_run_target(s(\"claude-spt\"), None, |_| None),\n*1870|            RunTarget::Picker { adapter: s(\"claude-spt\"), id: None }\n 1871|        );\n 1872|        // (None,None) → bare picker.\n 1873|        assert_eq!(\n 1874|            resolve_run_target(None, None, |_| None),\n*1875|            RunTarget::Picker { adapter: None, id: None }\n 1876|        );\n 1877|    }\n 1878|}\n...\n 3789|/// Shared by the qualified `id@node` arm and the A-3 bare-id `Act` dispatch.\n*3790|fn dispatch_wan_rest(target: &str, tag: &str, verb: &str) -> i32 {\n 3791|    match crate::wansend::wan_rest(target, tag) {\n 3792|        crate::wansend::WanRestOutcome::Edge { node, detail } => {\n 3793|            eprintln!(\"{verb}:{target} ({detail}) at {node}\");\n\n## tests/\n### active_only_never_relay_e2e.rs\n 97|\n*98|// [int->REQ-SEND-WINDOW-DRAIN-HONOR] a remote-only target: an `--active-only` send\n 99|// refuses LOCAL-ONLY (never crosses the WAN, which would strip the window class and\n 100|// relay-deliver it), while a `default` send to the SAME target takes the WAN leg —\n 101|// proving the local-only guard is `active_only`-specific.\n### boundary_ready_strand_e2e.rs\n 1|//! F-029 C-2 (REQ-HAZARD-BOUNDARY-READY-STRAND) — SEAM-2, B6's second half. A\n*2|//! `/clear` boundary must LEAVE the perch a live inject target: the pre-boundary\n 3|//! soft `api session-end` (which CC fires for the DEPARTING session before\n 4|//! SessionStart, while its sid still matches the pin) removes the ready marker; the\n 5|//! boundary must re-stamp it, or every post-clear force-native (incl. the checkpoint\n### gateway_e2e.rs\n 79|    home: &Path,\n*80|    target: &str,\n 81|    from: &str,\n 82|    session_id: &str,\n 83|    body: &str,"}],"details":{"scopePath":"crates/spt/src/cli.rs, crates/spt/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":23,"fileCount":4,"files":["crates/spt/src/cli.rs","crates/spt/tests/active_only_never_relay_e2e.rs","crates/spt/tests/boundary_ready_strand_e2e.rs","crates/spt/tests/gateway_e2e.rs"],"fileMatches":[{"path":"crates/spt/src/cli.rs","count":20},{"path":"crates/spt/tests/active_only_never_relay_e2e.rs","count":1},{"path":"crates/spt/tests/boundary_ready_strand_e2e.rs","count":1},{"path":"crates/spt/tests/gateway_e2e.rs","count":1}],"truncated":true,"perFileLimitReached":20,"displayContent":"# crates/spt/\n\n## src/\n### cli.rs\n   70│    /// Send a message (body read from stdin); fire-and-forget.\n  *71│    Send {\n   72│        // [impl->REQ-SEND-REPLYTO-REMOVE] `--reply-to` is hard-removed; `target` is\n   73│        // a required positional (no reply-to fallback). Reply-correlation rides the\n   74│        // structural `from`, not a flag.\n   75│        /// Target perch id.\n  *76│        target: String,\n   77│        /// Sender id carried structurally as the message `from` (auto-detected from session if omitted).\n   78│        #[arg(long)]\n   79│        from: Option<String>,\n     │...\n  119│        /// Target perch id.\n *120│        target: String,\n  121│        /// Sender id (auto-detected from session if omitted).\n  122│        #[arg(long)]\n  123│        from: Option<String>,\n     │...\n  819│        /// `<adapter>` or host `<binary>` whose pointer to drop.\n *820│        target: String,\n  821│        /// Clear the pointer for `target` instead of setting it.\n  822│        #[arg(long)]\n  823│        clear: bool,\n     │...\n  911│        /// The capability op + args (vocabulary-checked against the manifest).\n *912│        #[arg(trailing_var_arg = true)]\n  913│        op: Vec<String>,\n  914│        #[arg(long)]\n  915│        owner: Option<String>,\n     │...\n  950│    /// File transfers are progress-queryable by xfer id.\n *951│    Send {\n  952│        /// Canonical id or alias.\n  953│        shell_ref: String,\n  954│        /// The text payload.\n     │...\n 1157│        #[arg(long)]\n*1158│        target: Option<String>,\n 1159│        /// Issuer endpoint id (auto-detected from session if omitted).\n 1160│        #[arg(long)]\n 1161│        from: Option<String>,\n     │...\n 1289│    match command {\n*1290│        Cmd::Send {\n 1291│            target,\n 1292│            from,\n 1293│            idle_only,\n     │...\n 1411│                    // resolves it (resume info.cwd → current_dir).\n*1412│                    RunTarget::Direct { adapter, id } => cmd_endpoint_run(\n 1413│                        &adapter, &id, resume, None, start, attach, view, subnet.as_deref(), save,\n 1414│                    ),\n 1415│                    // Bare (or partial) → the interactive picker (REQ-RUN-PICKER).\n     │...\n 1417│                    // perch pre-fills the create-new id.\n*1418│                    RunTarget::Picker { adapter, id } => crate::picker::run(adapter, id),\n 1419│                }\n 1420│            }\n 1421│        },\n     │...\n 1822│    match (adapter, id) {\n*1823│        (Some(adapter), Some(id)) => RunTarget::Direct { adapter, id },\n 1824│        (None, Some(id)) => match recorded(&id) {\n*1825│            Some(adapter) => RunTarget::Direct { adapter, id },\n*1826│            None => RunTarget::Picker { adapter: None, id: Some(id) },\n 1827│        },\n*1828│        (adapter, id) => RunTarget::Picker { adapter, id },\n 1829│    }\n 1830│}\n 1831│\n     │...\n 1851│            resolve_run_target(s(\"claude-spt\"), s(\"foo\"), |_| panic!(\"must not read perch\")),\n*1852│            RunTarget::Direct { adapter: \"claude-spt\".into(), id: \"foo\".into() }\n 1853│        );\n 1854│        // (None,Some) + existing perch's recorded adapter → Direct (the D-1 reuse).\n 1855│        assert_eq!(\n     │...\n 1859│            }),\n*1860│            RunTarget::Direct { adapter: \"claude-spt\".into(), id: \"foo\".into() }\n 1861│        );\n 1862│        // (None,Some) + no perch / adapterless → today's create-new picker prefill.\n 1863│        assert_eq!(\n 1864│            resolve_run_target(None, s(\"foo\"), |_| None),\n*1865│            RunTarget::Picker { adapter: None, id: s(\"foo\") }\n 1866│        );\n 1867│        // (Some,None) → picker with the adapter pre-selected.\n 1868│        assert_eq!(\n 1869│            resolve_run_target(s(\"claude-spt\"), None, |_| None),\n*1870│            RunTarget::Picker { adapter: s(\"claude-spt\"), id: None }\n 1871│        );\n 1872│        // (None,None) → bare picker.\n 1873│        assert_eq!(\n 1874│            resolve_run_target(None, None, |_| None),\n*1875│            RunTarget::Picker { adapter: None, id: None }\n 1876│        );\n 1877│    }\n 1878│}\n     │...\n 3789│/// Shared by the qualified `id@node` arm and the A-3 bare-id `Act` dispatch.\n*3790│fn dispatch_wan_rest(target: &str, tag: &str, verb: &str) -> i32 {\n 3791│    match crate::wansend::wan_rest(target, tag) {\n 3792│        crate::wansend::WanRestOutcome::Edge { node, detail } => {\n 3793│            eprintln!(\"{verb}:{target} ({detail}) at {node}\");\n\n## tests/\n### active_only_never_relay_e2e.rs\n  97│\n *98│// [int->REQ-SEND-WINDOW-DRAIN-HONOR] a remote-only target: an `--active-only` send\n  99│// refuses LOCAL-ONLY (never crosses the WAN, which would strip the window class and\n 100│// relay-deliver it), while a `default` send to the SAME target takes the WAN leg —\n 101│// proving the local-only guard is `active_only`-specific.\n### boundary_ready_strand_e2e.rs\n 1│//! F-029 C-2 (REQ-HAZARD-BOUNDARY-READY-STRAND) — SEAM-2, B6's second half. A\n*2│//! `/clear` boundary must LEAVE the perch a live inject target: the pre-boundary\n 3│//! soft `api session-end` (which CC fires for the DEPARTING session before\n 4│//! SessionStart, while its sid still matches the pin) removes the ready marker; the\n 5│//! boundary must re-stamp it, or every post-clear force-native (incl. the checkpoint\n### gateway_e2e.rs\n 79│    home: &Path,\n*80│    target: &str,\n 81│    from: &str,\n 82│    session_id: &str,\n 83│    body: &str,"},"isError":false,"timestamp":1784197487251}}
{"type":"message","id":"a83dd6e5","parentId":"636f0775","timestamp":"2026-07-16T10:24:47.413Z","message":{"role":"toolResult","toolCallId":"call_MvMbCSenuNZDrzNf2zIdnA3E|fc_017bff223290b7ba016a58b16f3270819ab9be757244ce7c95","toolName":"read","content":[{"type":"text","text":"fn cmd_send(\n…\n) -> i32 {\n…\n    let outcome = if try_inject && !body.is_empty() && spt_daemon::is_running() {\n…\n    } else {\n        deliver::send_windowed(&target, &from, body, &owlery, window, ephemeral)\n    };\n\n    // REQ-IDLE-PARKED-DELIVERY (primary — send-time): a non-active-only send that spooled\n    // to an ALREADY-idle spt-hosted target will never get an ACTIVE→IDLE edge (the F-023\n    // idle-edge drain only fires on the transition), so it would park until an operator\n    // poke. Re-offer the just-spooled row through the inject leg NOW; the pulse-tick belt\n    // is the backstop. On a successful inject the send is honestly SENT, not QUEUED.\n    // [impl->REQ-IDLE-PARKED-DELIVERY]\n    let outcome = if matches!(outcome, SendOutcome::Queued)\n        && !active_only\n        && spt_daemon::is_running()\n        && crate::api::delivery::is_idle(&target)\n        && spt_daemon::inject::drain_idle_spool(&target, &owlery) > 0\n    {\n        SendOutcome::Sent\n    } else {\n        outcome\n    };\n\n    match outcome {\n        SendOutcome::Sent => {\n            ok_line(format!(\"SENT:{target}\"));\n            0\n        }\n        SendOutcome::Queued => {\n            let label = if active_only {\n                \"DEFERRED\"\n            } else if idle_only {\n                \"QUEUED(idle-only)\"\n            } else {\n                \"QUEUED\"\n            };\n            ok_line(format!(\"{}:{}\", label, target));\n            0\n        }\n        SendOutcome::NoPerch => {\n            if active_only {\n                // The wire record carries no window class (v1) — a remote\n                // active-only (hook-channel) send is a documented seam, not a\n                // silent reclass, so it stays local-only.\n                eprintln!(\n                    \"NO_PERCH:{} is not listening (active-only stays local-only)\",\n                    target\n                );\n                return 1;\n            }\n            // The WAN leg (D9-1-4): resolve against the registry snapshots\n            // and ship to the instance's node; its daemon delivers/spools.\n            // [impl->REQ-NET-1]\n            // #9/#10 (REQ-WAN-SEND-DELIVERY): SENT(WAN) is HONEST — printed ONLY\n            // on a receiver-confirmed delivery/spool/dedup. Refused / no-perch /\n            // unconfirmed each read as their own non-zero failure, so a stale\n            // route or a dropped payload can no longer masquerade as delivered.\n            use crate::wansend::WanSendOutcome;\n            match crate::wansend::wan_send(&target, &from, body) {\n                WanSendOutcome::Sent { node, how } => {\n                    // \"delivered\" is the plain confirmed case; annotate spool/dup.\n                    if how == \"delivered\" {\n                        ok_line(format!(\"SENT(WAN):{target}@{node}\"));\n                    } else {\n                        ok_line(format!(\"SENT(WAN):{target}@{node} ({how})\"));\n                    }\n                    0\n                }\n                WanSendOutcome::Refused { node } => {\n                    eprintln!(\"WAN_REFUSED:{target}@{node} — the receiver denied the message (access gate)\");\n                    1\n                }\n                WanSendOutcome::NoPerchRemote { node } => {\n                    eprintln!(\"WAN_NO_PERCH:{target} — no perch on {node} (stale route? the endpoint may have moved or stopped)\");\n                    1\n                }\n                WanSendOutcome::Unconfirmed { node } => {\n                    eprintln!(\"WAN_UNCONFIRMED:{target}@{node} — delivery not confirmed (peer did not ack; it may be offline or on an old version)\");\n                    1\n                }\n                WanSendOutcome::Ambiguous(why) => {\n                    eprintln!(\"AMBIGUOUS:{target} — {why}\");\n                    1\n                }\n                WanSendOutcome::NotFound => {\n                    eprintln!(\"NO_PERCH:{} is not listening\", target);\n                    1\n                }\n                WanSendOutcome::Failed(e) => {\n                    eprintln!(\"WAN_FAIL:{target} — {e}\");\n                    1\n                }\n            }\n        }\n        SendOutcome::Empty => {\n            eprintln!(\"EMPTY_MSG: refusing to send empty message to {}\", target);\n            1\n        }\n    }\n}\n\n/// Print a SUCCESS epilogue to stderr in legacy-spt blue. Epilogues live on\n/// stderr by contract (stdout carries message payloads), but PowerShell\n/// renders a native command's stderr red — success dressed as an error.\n/// Explicit color overrides that; a redirect/pipe (non-tty) or NO_COLOR gets\n/// plain text, so logs and machine consumers see the bare tag unchanged.\n/// `FORCE_COLOR` (any value but `0`) overrides the tty gate — captured\n/// contexts that still render ANSI (harness tool panes piping stderr) can\n/// opt back in. Failure lines stay uncolored — the host shell's red reads\n/// correctly there.\nfn ok_line(line: String) {\n    use std::io::IsTerminal;\n    let forced = std::env::var_os(\"FORCE_COLOR\").is_some_and(|v| v != \"0\");\n    let auto = std::env::var_os(\"NO_COLOR\").is_none() && std::io::stderr().is_terminal();\n    if forced || auto {\n        eprintln!(\"\\x1b[94m{line}\\x1b[0m\");\n    } else {\n        eprintln!(\"{line}\");\n    }\n}\n\nfn cmd_ring(target: String, from: Option<String>, timeout: u64) -> i32 {\n…\n}\n\n[Showing lines 6599-6718 of 18900. Use :6719 to continue]"}],"details":{"truncation":{"content":"    } else {\r\n        deliver::send_windowed(&target, &from, body, &owlery, window, ephemeral)\r\n    };\r\n\r\n    // REQ-IDLE-PARKED-DELIVERY (primary — send-time): a non-active-only send that spooled\r\n    // to an ALREADY-idle spt-hosted target will never get an ACTIVE→IDLE edge (the F-023\r\n    // idle-edge drain only fires on the transition), so it would park until an operator\r\n    // poke. Re-offer the just-spooled row through the inject leg NOW; the pulse-tick belt\r\n    // is the backstop. On a successful inject the send is honestly SENT, not QUEUED.\r\n    // [impl->REQ-IDLE-PARKED-DELIVERY]\r\n    let outcome = if matches!(outcome, SendOutcome::Queued)\r\n        && !active_only\r\n        && spt_daemon::is_running()\r\n        && crate::api::delivery::is_idle(&target)\r\n        && spt_daemon::inject::drain_idle_spool(&target, &owlery) > 0\r\n    {\r\n        SendOutcome::Sent\r\n    } else {\r\n        outcome\r\n    };\r\n\r\n    match outcome {\r\n        SendOutcome::Sent => {\r\n            ok_line(format!(\"SENT:{target}\"));\r\n            0\r\n        }\r\n        SendOutcome::Queued => {\r\n            let label = if active_only {\r\n                \"DEFERRED\"\r\n            } else if idle_only {\r\n                \"QUEUED(idle-only)\"\r\n            } else {\r\n                \"QUEUED\"\r\n            };\r\n            ok_line(format!(\"{}:{}\", label, target));\r\n            0\r\n        }\r\n        SendOutcome::NoPerch => {\r\n            if active_only {\r\n                // The wire record carries no window class (v1) — a remote\r\n                // active-only (hook-channel) send is a documented seam, not a\r\n                // silent reclass, so it stays local-only.\r\n                eprintln!(\r\n                    \"NO_PERCH:{} is not listening (active-only stays local-only)\",\r\n                    target\r\n                );\r\n                return 1;\r\n            }\r\n            // The WAN leg (D9-1-4): resolve against the registry snapshots\r\n            // and ship to the instance's node; its daemon delivers/spools.\r\n            // [impl->REQ-NET-1]\r\n            // #9/#10 (REQ-WAN-SEND-DELIVERY): SENT(WAN) is HONEST — printed ONLY\r\n            // on a receiver-confirmed delivery/spool/dedup. Refused / no-perch /\r\n            // unconfirmed each read as their own non-zero failure, so a stale\r\n            // route or a dropped payload can no longer masquerade as delivered.\r\n            use crate::wansend::WanSendOutcome;\r\n            match crate::wansend::wan_send(&target, &from, body) {\r\n                WanSendOutcome::Sent { node, how } => {\r\n                    // \"delivered\" is the plain confirmed case; annotate spool/dup.\r\n                    if how == \"delivered\" {\r\n                        ok_line(format!(\"SENT(WAN):{target}@{node}\"));\r\n                    } else {\r\n                        ok_line(format!(\"SENT(WAN):{target}@{node} ({how})\"));\r\n                    }\r\n                    0\r\n                }\r\n                WanSendOutcome::Refused { node } => {\r\n                    eprintln!(\"WAN_REFUSED:{target}@{node} — the receiver denied the message (access gate)\");\r\n                    1\r\n                }\r\n                WanSendOutcome::NoPerchRemote { node } => {\r\n                    eprintln!(\"WAN_NO_PERCH:{target} — no perch on {node} (stale route? the endpoint may have moved or stopped)\");\r\n                    1\r\n                }\r\n                WanSendOutcome::Unconfirmed { node } => {\r\n                    eprintln!(\"WAN_UNCONFIRMED:{target}@{node} — delivery not confirmed (peer did not ack; it may be offline or on an old version)\");\r\n                    1\r\n                }\r\n                WanSendOutcome::Ambiguous(why) => {\r\n                    eprintln!(\"AMBIGUOUS:{target} — {why}\");\r\n                    1\r\n                }\r\n                WanSendOutcome::NotFound => {\r\n                    eprintln!(\"NO_PERCH:{} is not listening\", target);\r\n                    1\r\n                }\r\n                WanSendOutcome::Failed(e) => {\r\n                    eprintln!(\"WAN_FAIL:{target} — {e}\");\r\n                    1\r\n                }\r\n            }\r\n        }\r\n        SendOutcome::Empty => {\r\n            eprintln!(\"EMPTY_MSG: refusing to send empty message to {}\", target);\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// Print a SUCCESS epilogue to stderr in legacy-spt blue. Epilogues live on\r\n/// stderr by contract (stdout carries message payloads), but PowerShell\r\n/// renders a native command's stderr red — success dressed as an error.\r\n/// Explicit color overrides that; a redirect/pipe (non-tty) or NO_COLOR gets\r\n/// plain text, so logs and machine consumers see the bare tag unchanged.\r\n/// `FORCE_COLOR` (any value but `0`) overrides the tty gate — captured\r\n/// contexts that still render ANSI (harness tool panes piping stderr) can\r\n/// opt back in. Failure lines stay uncolored — the host shell's red reads\r\n/// correctly there.\r\nfn ok_line(line: String) {\r\n    use std::io::IsTerminal;\r\n    let forced = std::env::var_os(\"FORCE_COLOR\").is_some_and(|v| v != \"0\");\r\n    let auto = std::env::var_os(\"NO_COLOR\").is_none() && std::io::stderr().is_terminal();\r\n    if forced || auto {\r\n        eprintln!(\"\\x1b[94m{line}\\x1b[0m\");\r\n    } else {\r\n        eprintln!(\"{line}\");\r\n    }\r\n}\r\n\r\nfn cmd_ring(target: String, from: Option<String>, timeout: u64) -> i32 {\r","truncated":true,"truncatedBy":"lines","totalLines":12302,"totalBytes":5335,"outputLines":120,"outputBytes":5335,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"fn cmd_send(\n…\n) -> i32 {\n…\n    let outcome = if try_inject && !body.is_empty() && spt_daemon::is_running() {\n…\n    } else {\r\n        deliver::send_windowed(&target, &from, body, &owlery, window, ephemeral)\r\n    };\r\n\r\n    // REQ-IDLE-PARKED-DELIVERY (primary — send-time): a non-active-only send that spooled\r\n    // to an ALREADY-idle spt-hosted target will never get an ACTIVE→IDLE edge (the F-023\r\n    // idle-edge drain only fires on the transition), so it would park until an operator\r\n    // poke. Re-offer the just-spooled row through the inject leg NOW; the pulse-tick belt\r\n    // is the backstop. On a successful inject the send is honestly SENT, not QUEUED.\r\n    // [impl->REQ-IDLE-PARKED-DELIVERY]\r\n    let outcome = if matches!(outcome, SendOutcome::Queued)\r\n        && !active_only\r\n        && spt_daemon::is_running()\r\n        && crate::api::delivery::is_idle(&target)\r\n        && spt_daemon::inject::drain_idle_spool(&target, &owlery) > 0\r\n    {\r\n        SendOutcome::Sent\r\n    } else {\r\n        outcome\r\n    };\r\n\r\n    match outcome {\r\n        SendOutcome::Sent => {\r\n            ok_line(format!(\"SENT:{target}\"));\r\n            0\r\n        }\r\n        SendOutcome::Queued => {\r\n            let label = if active_only {\r\n                \"DEFERRED\"\r\n            } else if idle_only {\r\n                \"QUEUED(idle-only)\"\r\n            } else {\r\n                \"QUEUED\"\r\n            };\r\n            ok_line(format!(\"{}:{}\", label, target));\r\n            0\r\n        }\r\n        SendOutcome::NoPerch => {\r\n            if active_only {\r\n                // The wire record carries no window class (v1) — a remote\r\n                // active-only (hook-channel) send is a documented seam, not a\r\n                // silent reclass, so it stays local-only.\r\n                eprintln!(\r\n                    \"NO_PERCH:{} is not listening (active-only stays local-only)\",\r\n                    target\r\n                );\r\n                return 1;\r\n            }\r\n            // The WAN leg (D9-1-4): resolve against the registry snapshots\r\n            // and ship to the instance's node; its daemon delivers/spools.\r\n            // [impl->REQ-NET-1]\r\n            // #9/#10 (REQ-WAN-SEND-DELIVERY): SENT(WAN) is HONEST — printed ONLY\r\n            // on a receiver-confirmed delivery/spool/dedup. Refused / no-perch /\r\n            // unconfirmed each read as their own non-zero failure, so a stale\r\n            // route or a dropped payload can no longer masquerade as delivered.\r\n            use crate::wansend::WanSendOutcome;\r\n            match crate::wansend::wan_send(&target, &from, body) {\r\n                WanSendOutcome::Sent { node, how } => {\r\n                    // \"delivered\" is the plain confirmed case; annotate spool/dup.\r\n                    if how == \"delivered\" {\r\n                        ok_line(format!(\"SENT(WAN):{target}@{node}\"));\r\n                    } else {\r\n                        ok_line(format!(\"SENT(WAN):{target}@{node} ({how})\"));\r\n                    }\r\n                    0\r\n                }\r\n                WanSendOutcome::Refused { node } => {\r\n                    eprintln!(\"WAN_REFUSED:{target}@{node} — the receiver denied the message (access gate)\");\r\n                    1\r\n                }\r\n                WanSendOutcome::NoPerchRemote { node } => {\r\n                    eprintln!(\"WAN_NO_PERCH:{target} — no perch on {node} (stale route? the endpoint may have moved or stopped)\");\r\n                    1\r\n                }\r\n                WanSendOutcome::Unconfirmed { node } => {\r\n                    eprintln!(\"WAN_UNCONFIRMED:{target}@{node} — delivery not confirmed (peer did not ack; it may be offline or on an old version)\");\r\n                    1\r\n                }\r\n                WanSendOutcome::Ambiguous(why) => {\r\n                    eprintln!(\"AMBIGUOUS:{target} — {why}\");\r\n                    1\r\n                }\r\n                WanSendOutcome::NotFound => {\r\n                    eprintln!(\"NO_PERCH:{} is not listening\", target);\r\n                    1\r\n                }\r\n                WanSendOutcome::Failed(e) => {\r\n                    eprintln!(\"WAN_FAIL:{target} — {e}\");\r\n                    1\r\n                }\r\n            }\r\n        }\r\n        SendOutcome::Empty => {\r\n            eprintln!(\"EMPTY_MSG: refusing to send empty message to {}\", target);\r\n            1\r\n        }\r\n    }\r\n}\r\n\r\n/// Print a SUCCESS epilogue to stderr in legacy-spt blue. Epilogues live on\r\n/// stderr by contract (stdout carries message payloads), but PowerShell\r\n/// renders a native command's stderr red — success dressed as an error.\r\n/// Explicit color overrides that; a redirect/pipe (non-tty) or NO_COLOR gets\r\n/// plain text, so logs and machine consumers see the bare tag unchanged.\r\n/// `FORCE_COLOR` (any value but `0`) overrides the tty gate — captured\r\n/// contexts that still render ANSI (harness tool panes piping stderr) can\r\n/// opt back in. Failure lines stay uncolored — the host shell's red reads\r\n/// correctly there.\r\nfn ok_line(line: String) {\r\n    use std::io::IsTerminal;\r\n    let forced = std::env::var_os(\"FORCE_COLOR\").is_some_and(|v| v != \"0\");\r\n    let auto = std::env::var_os(\"NO_COLOR\").is_none() && std::io::stderr().is_terminal();\r\n    if forced || auto {\r\n        eprintln!(\"\\x1b[94m{line}\\x1b[0m\");\r\n    } else {\r\n        eprintln!(\"{line}\");\r\n    }\r\n}\r\n\r\nfn cmd_ring(target: String, from: Option<String>, timeout: u64) -> i32 {\r\n…\n}","startLine":6455,"lineNumbers":[6455,null,6465,null,6592,null,6599,6600,6601,6602,6603,6604,6605,6606,6607,6608,6609,6610,6611,6612,6613,6614,6615,6616,6617,6618,6619,6620,6621,6622,6623,6624,6625,6626,6627,6628,6629,6630,6631,6632,6633,6634,6635,6636,6637,6638,6639,6640,6641,6642,6643,6644,6645,6646,6647,6648,6649,6650,6651,6652,6653,6654,6655,6656,6657,6658,6659,6660,6661,6662,6663,6664,6665,6666,6667,6668,6669,6670,6671,6672,6673,6674,6675,6676,6677,6678,6679,6680,6681,6682,6683,6684,6685,6686,6687,6688,6689,6690,6691,6692,6693,6694,6695,6696,6697,6698,6699,6700,6701,6702,6703,6704,6705,6706,6707,6708,6709,6710,6711,6712,6713,6714,6715,6716,6717,6718,null,6764]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":18900,"totalBytes":5335,"outputLines":120,"outputBytes":5335,"shownRange":{"start":6599,"end":6718},"nextOffset":6719}}},"isError":false,"timestamp":1784197487413}}
{"type":"message","id":"71435e34","parentId":"a83dd6e5","timestamp":"2026-07-16T10:24:47.415Z","message":{"role":"toolResult","toolCallId":"call_6c488zycC7ITG5e4kpovvLga|fc_017bff223290b7ba016a58b16f3288819abcb306864126f3b7","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/tests/\n### gateway_e2e.rs\n 65|\n*66|/// Create an offline perch (dir + info.json, no listener) so a send to it spools.\n 67|fn offline_peer(id: &str) -> PathBuf {\n 68|    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 69|    std::fs::create_dir_all(&perch_path).unwrap();\n### notify_shell_e2e.rs\n 115|    std::fs::write(perch::resolve_ready_file(\"doyle\", ParentHint::Infer), \"\").unwrap();\n*116|    let _listener = spt_msg::listener::PollListener::bind(\"doyle\").expect(\"owner listener\");\n 117|    spt_store::nodeid::load_or_create().expect(\"node identity\");\n 118|\n 119|    // ── spawn → bind: the real binary launches detached and onlines its perch.\n### poll_envelope_e2e.rs\n 54|\n*55|    // Two offline sends spool (the perch exists but holds no live listener).\n 56|    // The first body is multi-LINE on purpose: it must arrive as ONE whole\n 57|    // envelope (newline escaped to <br>), proving self-delimiting framing.\n 58|    let q1 = send(home, &[\"send\", \"doyle\", \"--from\", \"alice\"], \"line one\\nline two\");\n*59|    assert!(q1.contains(\"QUEUED:doyle\"), \"first send spools: {q1:?}\");\n 60|    let q2 = send(home, &[\"send\", \"doyle\", \"--from\", \"bob\"], \"second message\");\n*61|    assert!(q2.contains(\"QUEUED:doyle\"), \"second send spools: {q2:?}\");\n 62|\n 63|    // Drain via the hook channel. Each message is one whole <EVENT> line.\n 64|    // (`api` requires --adapter on every machinery invocation, REQ-API-1.) The\n### quickstart_e2e.rs\n 92|    assert!(\n*93|        out.contains(\"SENT:sergey\"),\n*94|        \"live delivery must report SENT:sergey, got {out:?}\"\n 95|    );\n 96|    // The documented receive shape: one EVENT envelope line on sergey's\n 97|    // stdout — from attribute carries the sender, body rides inside.\n...\n 115|    assert!(\n*116|        out.contains(\"QUEUED:sergey\"),\n*117|        \"offline delivery must report QUEUED:sergey, got {out:?}\"\n 118|    );\n 119|\n 120|    // ---- sergey comes back: the backlog drains (`ready --once`) ----\n### shell_actgate_e2e.rs\n 127|    std::fs::write(perch::resolve_ready_file(\"doyle\", ParentHint::Infer), \"\").unwrap();\n*128|    let _listener = spt_msg::listener::PollListener::bind(\"doyle\").expect(\"owner listener\");\n 129|    spt_store::nodeid::load_or_create().expect(\"node identity\");\n 130|\n 131|    // ── spawn (ungated) → online.\n### shell_e2e.rs\n 139|    std::fs::write(perch::resolve_ready_file(\"doyle\", ParentHint::Infer), \"\").unwrap();\n*140|    let listener = spt_msg::listener::PollListener::bind(\"doyle\").expect(\"owner listener\");\n 141|    spt_store::nodeid::load_or_create().expect(\"node identity\");\n 142|\n 143|    // ── spawn → bind: the real binary launches detached and its\n\n## spt-msg/src/\n### deliver.rs\n 165|\n*166|/// Deferred send (the hook channel): spool-only, **no TCP, no listener wake**.\n 167|/// The row is tagged `deferred=1` so the live event-stream drain\n 168|/// (`drain_non_deferred_at`) skips it — it surfaces only when a hook consumer\n 169|/// drains everything (`drain_all_at` / `peek_all_at`) on its natural cadence.\n...\n 233|    use super::*;\n*234|    use crate::listener::PollListener;\n 235|    use spt_store::info;\n 236|    use std::net::SocketAddr;\n 237|\n...\n 253|        let id = unique_id(\"test-online\");\n*254|        let listener = PollListener::bind(&id).unwrap();\n 255|\n 256|        let path = deliver(&id, \"sender\", \"live msg\", &owlery);\n 257|        assert_eq!(path, DeliveryPath::Tcp);\n...\n 276|    // local-TCP skip): an `active_only` send to a target with a LIVE relay\n*277|    // (PollListener bound — an idle relay-bearing live agent like lia) must NEVER\n 278|    // deliver over TCP. `active_only` is the poll-only hook channel; its contract is\n 279|    // \"never wakes / never reaches a relay\". It SPOOLS instead (poll-only), where the\n 280|    // relay backlog drain excludes it (GUARD 3). This locks the invariant so a\n...\n 293|        info::write_info(&perch_path, &rec).unwrap();\n*294|        let listener = PollListener::bind(&id).unwrap();\n 295|\n 296|        let outcome =\n 297|            send_windowed(&id, \"peer\", \"background ctx\", &owlery, spool::WINDOW_ACTIVE_ONLY, false);\n...\n 373|        let id = unique_id(\"test-send-online\");\n*374|        let _listener = PollListener::bind(&id).unwrap();\n 375|        // Need a perch dir for perch_exists; bind doesn't create info.json, but\n 376|        // is_online is true (ready... actually bind writes no ready marker).\n 377|        // Create ready + info so the online path is exercised deterministically.\n### listener.rs\n*1|//! `PollListener` — a loopback TCP accept listener with registry lifecycle.\n 2|//!\n 3|//! Clean-room re-expression of the sister `common/listener.rs` against the\n 4|//! spt-core `spt-store` API (perch resolver + registry). The socket plumbing\n...\n 8|//!\n*9|//! Lifecycle: [`PollListener::bind`] grabs an ephemeral `127.0.0.1:0` port and\n 10|//! writes the perch's address into the registry under the owning pid. On drop\n*11|//! (or [`PollListener::close`]) the registry entry is removed so no stale\n 12|//! address survives a crashed listener — the recycled-pid guard\n 13|//! ([`spt_store::registry::resolve_address`]) is the second line of defense.\n 14|//!\n...\n 31|/// A loopback TCP listener that owns its registry registration.\n*32|pub struct PollListener {\n 33|    listener: TcpListener,\n 34|    id: String,\n 35|    owlery: PathBuf,\n...\n 78|\n*79|impl PollListener {\n 80|    /// Bind an ephemeral loopback port in **non-blocking** mode and register the\n 81|    /// resulting address under this perch id (owning pid = current process).\n 82|    pub fn bind(id: &str) -> Result<Self, String> {\n...\n 158|    /// Unregister and stamp `info.json` with the `BUSY` pid sentinel — the\n*159|    /// \"listener alive but between polls\" state a sender may observe. Used when\n 160|    /// the perch persists but the socket is torn down mid-cycle.\n 161|    pub fn close_busy(&mut self) {\n 162|        let _ = registry::unregister_address(&self.id, &self.owlery);\n...\n 167|\n*168|impl Drop for PollListener {\n 169|    fn drop(&mut self) {\n 170|        if !self.closed {\n 171|            // Default teardown: drop the registry address so no stale entry\n...\n 209|        let id = unique_id(\"test-bind\");\n*210|        let listener = PollListener::bind(&id).unwrap();\n 211|        let addr = listener.local_addr().unwrap();\n 212|\n 213|        // Registry holds the bound address.\n...\n 244|        {\n*245|            let _l = PollListener::bind(&id).unwrap();\n 246|            assert!(registry::lookup_address(&id, &owlery).is_some());\n 247|        }\n 248|        assert!(registry::lookup_address(&id, &owlery).is_none());\n...\n 256|        let id = unique_id(\"test-block-idle\");\n*257|        let listener = PollListener::bind_blocking(&id, Duration::from_millis(100)).unwrap();\n 258|        let start = Instant::now();\n 259|        assert!(listener.check_message_blocking().is_none());\n 260|        assert!(start.elapsed() < Duration::from_millis(400));\n...\n 268|        let id = unique_id(\"test-block-recv\");\n*269|        let listener = PollListener::bind_blocking(&id, Duration::from_millis(500)).unwrap();\n 270|        let addr = listener.local_addr().unwrap();\n 271|\n 272|        let h = std::thread::spawn(move || {\n### ready.rs\n 12|//!    `ready` marker.\n*13|//! 2. Bind a loopback listener and register its address (via [`PollListener`]).\n 14|//! 3. Drain the **non-deferred** spool backlog so messages that arrived while\n 15|//!    the agent was offline are delivered on startup. Deferred rows are left\n 16|//!    for the hook channel (REQ-HAZARD-DEFERRED-SURVIVE-DRAIN, covered M0).\n...\n 29|\n*30|use crate::listener::PollListener;\n 31|\n 32|/// Default poll window for [`ReadyAgent::recv`]; bounds each blocking accept so\n 33|/// the caller stays responsive (and can re-check spool / shut down between calls).\n...\n 38|    id: String,\n*39|    listener: PollListener,\n 40|    perch_path: PathBuf,\n 41|    closed: bool,\n 42|}\n...\n 145|        // recv() stays responsive.\n*146|        let listener = PollListener::bind_blocking(id, RECV_POLL)?;\n 147|\n 148|        // Drain the offline backlog (non-deferred only; deferred rows survive\n 149|        // for the hook channel). Carried structurally as (from, body) — the\n...\n 183|    /// Drain any non-deferred spool rows that accrued since startup (e.g. a\n*184|    /// sender that spooled because it raced the listener bind, or a hook-channel\n 185|    /// arrival), as `(from, body)`. Oldest first.\n 186|    pub fn drain_spool(&self) -> Vec<(String, String)> {\n 187|        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg (ready-agent since-startup drain).\n### ring.rs\n 23|use crate::deliver;\n*24|use crate::listener::PollListener;\n 25|\n 26|/// How often the reply wait polls its listener / spool.\n 27|const REPLY_POLL: Duration = Duration::from_millis(50);\n...\n 124|/// back over TCP.\n*125|fn setup_ephemeral(from: &str) -> Result<PollListener, String> {\n 126|    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 127|    fs::create_dir_all(&perch_path).map_err(|e| format!(\"create ephemeral perch: {}\", e))?;\n 128|    let rec = InfoJson::new(from, \"once\", std::process::id(), \"ring\", \"ready_agent\");\n...\n 131|        .map_err(|e| format!(\"write ready marker: {}\", e))?;\n*132|    PollListener::bind_blocking(from, REPLY_POLL)\n 133|}\n 134|\n 135|/// Remove an ephemeral perch entirely: ready marker, info.json, spool db, then\n\n## spt-daemon/tests/\n### attach.rs\n 808|    let mut decoder = AttachDecoder::new();\n*809|    let mut sent: Vec<u8> = Vec::new(); // bytes life 1 pumped onto the wire\n 810|    'life1: loop {\n 811|        match life1.read_event().expect(\"life1 event\") {\n 812|            BrokerEvent::NetStreamData {"}],"details":{"scopePath":"crates/spt/tests, crates/spt-msg/src, crates/spt-daemon/tests","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":37,"fileCount":11,"files":["crates/spt/tests/gateway_e2e.rs","crates/spt/tests/notify_shell_e2e.rs","crates/spt/tests/poll_envelope_e2e.rs","crates/spt/tests/quickstart_e2e.rs","crates/spt/tests/shell_actgate_e2e.rs","crates/spt/tests/shell_e2e.rs","crates/spt-msg/src/deliver.rs","crates/spt-msg/src/listener.rs","crates/spt-msg/src/ready.rs","crates/spt-msg/src/ring.rs","crates/spt-daemon/tests/attach.rs"],"fileMatches":[{"path":"crates/spt/tests/gateway_e2e.rs","count":1},{"path":"crates/spt/tests/notify_shell_e2e.rs","count":1},{"path":"crates/spt/tests/poll_envelope_e2e.rs","count":3},{"path":"crates/spt/tests/quickstart_e2e.rs","count":4},{"path":"crates/spt/tests/shell_actgate_e2e.rs","count":1},{"path":"crates/spt/tests/shell_e2e.rs","count":1},{"path":"crates/spt-msg/src/deliver.rs","count":6},{"path":"crates/spt-msg/src/listener.rs","count":11},{"path":"crates/spt-msg/src/ready.rs","count":5},{"path":"crates/spt-msg/src/ring.rs","count":3},{"path":"crates/spt-daemon/tests/attach.rs","count":1}],"truncated":false,"displayContent":"# crates/\n\n## spt/tests/\n### gateway_e2e.rs\n 65│\n*66│/// Create an offline perch (dir + info.json, no listener) so a send to it spools.\n 67│fn offline_peer(id: &str) -> PathBuf {\n 68│    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 69│    std::fs::create_dir_all(&perch_path).unwrap();\n### notify_shell_e2e.rs\n 115│    std::fs::write(perch::resolve_ready_file(\"doyle\", ParentHint::Infer), \"\").unwrap();\n*116│    let _listener = spt_msg::listener::PollListener::bind(\"doyle\").expect(\"owner listener\");\n 117│    spt_store::nodeid::load_or_create().expect(\"node identity\");\n 118│\n 119│    // ── spawn → bind: the real binary launches detached and onlines its perch.\n### poll_envelope_e2e.rs\n 54│\n*55│    // Two offline sends spool (the perch exists but holds no live listener).\n 56│    // The first body is multi-LINE on purpose: it must arrive as ONE whole\n 57│    // envelope (newline escaped to <br>), proving self-delimiting framing.\n 58│    let q1 = send(home, &[\"send\", \"doyle\", \"--from\", \"alice\"], \"line one\\nline two\");\n*59│    assert!(q1.contains(\"QUEUED:doyle\"), \"first send spools: {q1:?}\");\n 60│    let q2 = send(home, &[\"send\", \"doyle\", \"--from\", \"bob\"], \"second message\");\n*61│    assert!(q2.contains(\"QUEUED:doyle\"), \"second send spools: {q2:?}\");\n 62│\n 63│    // Drain via the hook channel. Each message is one whole <EVENT> line.\n 64│    // (`api` requires --adapter on every machinery invocation, REQ-API-1.) The\n### quickstart_e2e.rs\n  92│    assert!(\n *93│        out.contains(\"SENT:sergey\"),\n *94│        \"live delivery must report SENT:sergey, got {out:?}\"\n  95│    );\n  96│    // The documented receive shape: one EVENT envelope line on sergey's\n  97│    // stdout — from attribute carries the sender, body rides inside.\n    │...\n 115│    assert!(\n*116│        out.contains(\"QUEUED:sergey\"),\n*117│        \"offline delivery must report QUEUED:sergey, got {out:?}\"\n 118│    );\n 119│\n 120│    // ---- sergey comes back: the backlog drains (`ready --once`) ----\n### shell_actgate_e2e.rs\n 127│    std::fs::write(perch::resolve_ready_file(\"doyle\", ParentHint::Infer), \"\").unwrap();\n*128│    let _listener = spt_msg::listener::PollListener::bind(\"doyle\").expect(\"owner listener\");\n 129│    spt_store::nodeid::load_or_create().expect(\"node identity\");\n 130│\n 131│    // ── spawn (ungated) → online.\n### shell_e2e.rs\n 139│    std::fs::write(perch::resolve_ready_file(\"doyle\", ParentHint::Infer), \"\").unwrap();\n*140│    let listener = spt_msg::listener::PollListener::bind(\"doyle\").expect(\"owner listener\");\n 141│    spt_store::nodeid::load_or_create().expect(\"node identity\");\n 142│\n 143│    // ── spawn → bind: the real binary launches detached and its\n\n## spt-msg/src/\n### deliver.rs\n 165│\n*166│/// Deferred send (the hook channel): spool-only, **no TCP, no listener wake**.\n 167│/// The row is tagged `deferred=1` so the live event-stream drain\n 168│/// (`drain_non_deferred_at`) skips it — it surfaces only when a hook consumer\n 169│/// drains everything (`drain_all_at` / `peek_all_at`) on its natural cadence.\n    │...\n 233│    use super::*;\n*234│    use crate::listener::PollListener;\n 235│    use spt_store::info;\n 236│    use std::net::SocketAddr;\n 237│\n    │...\n 253│        let id = unique_id(\"test-online\");\n*254│        let listener = PollListener::bind(&id).unwrap();\n 255│\n 256│        let path = deliver(&id, \"sender\", \"live msg\", &owlery);\n 257│        assert_eq!(path, DeliveryPath::Tcp);\n    │...\n 276│    // local-TCP skip): an `active_only` send to a target with a LIVE relay\n*277│    // (PollListener bound — an idle relay-bearing live agent like lia) must NEVER\n 278│    // deliver over TCP. `active_only` is the poll-only hook channel; its contract is\n 279│    // \"never wakes / never reaches a relay\". It SPOOLS instead (poll-only), where the\n 280│    // relay backlog drain excludes it (GUARD 3). This locks the invariant so a\n    │...\n 293│        info::write_info(&perch_path, &rec).unwrap();\n*294│        let listener = PollListener::bind(&id).unwrap();\n 295│\n 296│        let outcome =\n 297│            send_windowed(&id, \"peer\", \"background ctx\", &owlery, spool::WINDOW_ACTIVE_ONLY, false);\n    │...\n 373│        let id = unique_id(\"test-send-online\");\n*374│        let _listener = PollListener::bind(&id).unwrap();\n 375│        // Need a perch dir for perch_exists; bind doesn't create info.json, but\n 376│        // is_online is true (ready... actually bind writes no ready marker).\n 377│        // Create ready + info so the online path is exercised deterministically.\n### listener.rs\n  *1│//! `PollListener` — a loopback TCP accept listener with registry lifecycle.\n   2│//!\n   3│//! Clean-room re-expression of the sister `common/listener.rs` against the\n   4│//! spt-core `spt-store` API (perch resolver + registry). The socket plumbing\n    │...\n   8│//!\n  *9│//! Lifecycle: [`PollListener::bind`] grabs an ephemeral `127.0.0.1:0` port and\n  10│//! writes the perch's address into the registry under the owning pid. On drop\n *11│//! (or [`PollListener::close`]) the registry entry is removed so no stale\n  12│//! address survives a crashed listener — the recycled-pid guard\n  13│//! ([`spt_store::registry::resolve_address`]) is the second line of defense.\n  14│//!\n    │...\n  31│/// A loopback TCP listener that owns its registry registration.\n *32│pub struct PollListener {\n  33│    listener: TcpListener,\n  34│    id: String,\n  35│    owlery: PathBuf,\n    │...\n  78│\n *79│impl PollListener {\n  80│    /// Bind an ephemeral loopback port in **non-blocking** mode and register the\n  81│    /// resulting address under this perch id (owning pid = current process).\n  82│    pub fn bind(id: &str) -> Result<Self, String> {\n    │...\n 158│    /// Unregister and stamp `info.json` with the `BUSY` pid sentinel — the\n*159│    /// \"listener alive but between polls\" state a sender may observe. Used when\n 160│    /// the perch persists but the socket is torn down mid-cycle.\n 161│    pub fn close_busy(&mut self) {\n 162│        let _ = registry::unregister_address(&self.id, &self.owlery);\n    │...\n 167│\n*168│impl Drop for PollListener {\n 169│    fn drop(&mut self) {\n 170│        if !self.closed {\n 171│            // Default teardown: drop the registry address so no stale entry\n    │...\n 209│        let id = unique_id(\"test-bind\");\n*210│        let listener = PollListener::bind(&id).unwrap();\n 211│        let addr = listener.local_addr().unwrap();\n 212│\n 213│        // Registry holds the bound address.\n    │...\n 244│        {\n*245│            let _l = PollListener::bind(&id).unwrap();\n 246│            assert!(registry::lookup_address(&id, &owlery).is_some());\n 247│        }\n 248│        assert!(registry::lookup_address(&id, &owlery).is_none());\n    │...\n 256│        let id = unique_id(\"test-block-idle\");\n*257│        let listener = PollListener::bind_blocking(&id, Duration::from_millis(100)).unwrap();\n 258│        let start = Instant::now();\n 259│        assert!(listener.check_message_blocking().is_none());\n 260│        assert!(start.elapsed() < Duration::from_millis(400));\n    │...\n 268│        let id = unique_id(\"test-block-recv\");\n*269│        let listener = PollListener::bind_blocking(&id, Duration::from_millis(500)).unwrap();\n 270│        let addr = listener.local_addr().unwrap();\n 271│\n 272│        let h = std::thread::spawn(move || {\n### ready.rs\n  12│//!    `ready` marker.\n *13│//! 2. Bind a loopback listener and register its address (via [`PollListener`]).\n  14│//! 3. Drain the **non-deferred** spool backlog so messages that arrived while\n  15│//!    the agent was offline are delivered on startup. Deferred rows are left\n  16│//!    for the hook channel (REQ-HAZARD-DEFERRED-SURVIVE-DRAIN, covered M0).\n    │...\n  29│\n *30│use crate::listener::PollListener;\n  31│\n  32│/// Default poll window for [`ReadyAgent::recv`]; bounds each blocking accept so\n  33│/// the caller stays responsive (and can re-check spool / shut down between calls).\n    │...\n  38│    id: String,\n *39│    listener: PollListener,\n  40│    perch_path: PathBuf,\n  41│    closed: bool,\n  42│}\n    │...\n 145│        // recv() stays responsive.\n*146│        let listener = PollListener::bind_blocking(id, RECV_POLL)?;\n 147│\n 148│        // Drain the offline backlog (non-deferred only; deferred rows survive\n 149│        // for the hook channel). Carried structurally as (from, body) — the\n    │...\n 183│    /// Drain any non-deferred spool rows that accrued since startup (e.g. a\n*184│    /// sender that spooled because it raced the listener bind, or a hook-channel\n 185│    /// arrival), as `(from, body)`. Oldest first.\n 186│    pub fn drain_spool(&self) -> Vec<(String, String)> {\n 187│        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg (ready-agent since-startup drain).\n### ring.rs\n  23│use crate::deliver;\n *24│use crate::listener::PollListener;\n  25│\n  26│/// How often the reply wait polls its listener / spool.\n  27│const REPLY_POLL: Duration = Duration::from_millis(50);\n    │...\n 124│/// back over TCP.\n*125│fn setup_ephemeral(from: &str) -> Result<PollListener, String> {\n 126│    let perch_path = perch::resolve_perch_path(from, ParentHint::Infer);\n 127│    fs::create_dir_all(&perch_path).map_err(|e| format!(\"create ephemeral perch: {}\", e))?;\n 128│    let rec = InfoJson::new(from, \"once\", std::process::id(), \"ring\", \"ready_agent\");\n    │...\n 131│        .map_err(|e| format!(\"write ready marker: {}\", e))?;\n*132│    PollListener::bind_blocking(from, REPLY_POLL)\n 133│}\n 134│\n 135│/// Remove an ephemeral perch entirely: ready marker, info.json, spool db, then\n\n## spt-daemon/tests/\n### attach.rs\n 808│    let mut decoder = AttachDecoder::new();\n*809│    let mut sent: Vec<u8> = Vec::new(); // bytes life 1 pumped onto the wire\n 810│    'life1: loop {\n 811│        match life1.read_event().expect(\"life1 event\") {\n 812│            BrokerEvent::NetStreamData {"},"isError":false,"timestamp":1784197487415}}
{"type":"message","id":"40a66d0e","parentId":"71435e34","timestamp":"2026-07-16T10:24:54.167Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reviewing quickstart integration seam**"},{"type":"toolCall","id":"call_Pu1uyqaBAo7HtfdDHDTcaTYj|fc_017bff223290b7ba016a58b1760e70819abcd7f6c935d37363","name":"read","arguments":{"i":"Reading quickstart integration seam","path":"crates/spt/tests/quickstart_e2e.rs","selector":"1-145"}},{"type":"toolCall","id":"call_A5BoSnoEmfEbw5dAcbk30PT8|fc_017bff223290b7ba016a58b1760e80819a817cecc47106bffb","name":"read","arguments":{"i":"Reading ready receive implementation","path":"crates/spt-msg/src/ready.rs","selector":"35-215"}},{"type":"toolCall","id":"call_Cpwv3TEFNFJFSmZuZwnqEZIK|fc_017bff223290b7ba016a58b1760e84819a80eac76c9c11ec6c","name":"grep","arguments":{"i":"Finding id validation hyphens","pattern":"validate_endpoint_id|INVALID_ID|ASCII|hyphen|'-'","path":"crates/spt-proto/src/id.rs; crates/spt/src/cli.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_oeQfuiR0TWTRCt6Bcr3LVSYe|fc_017bff223290b7ba016a58b1760e8c819ab1f5d47200959cfe","name":"read","arguments":{"i":"Reading endpoint id grammar","path":"crates/spt-proto/src/id.rs","selector":"1-180"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":10594,"output":218,"cacheRead":184832,"cacheWrite":0,"totalTokens":195644,"reasoningTokens":17,"cost":{"input":0.05297,"output":0.00654,"cacheRead":0.092416,"cacheWrite":0,"total":0.151926}},"stopReason":"toolUse","timestamp":1784197487450,"responseId":"resp_017bff223290b7ba016a58b16fff58819abc416f0722b98431","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b1721ac0819aaef1dcebfc17fa4f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLF2RdJzqLAyF4iijX3R6f_bMhMXIZQ3FjcrBI7XOEhni-oBm2YmAibyvnCE26j_XECsOU-_Z422ZRM6xRq9Y5e4a3BXw45U7osE8f9iMPHUGp5fe9MRT7llIVIyWpL89p6VdEgNsy_rIgv1KEGX-74wOECTpSOYhJi2JOtFm9DQh9rvXXun29KjL5zIsROrmd_9Xuki67xBp1v0cKpTZP_xyNeD6HHSb3vLgt6s1jQw288aJwdoa5sZK61MShJkNl1QgVTgqA86U-azNLEc6O2M6mhUwQqquRVlwniOqxoQxPirdjrdY2PfDJOHCpxXp5HTjcbCzlveS83o-3_OC0ueJpIghIK3MC-fKx6f6fmBos-5IJh00j4K6Nj0qqhWjzSJ8iNk0fu4D1XetjOX0udHHVaZ46D2y1xYKdxzBCNy4EW81cVYD8ZTcnVPW8VY7Qn9NNMnM_hDBWM7DHTuhgO5jF_PaYlXFIBTq3pzBgNZq8XNmMpXn-Lxk8CdUmZsEQzqa1OOWKbGJm_K4838hFcaN6DdOYA-WYrHVz8jKWrm-0ecWx0xGpe-nVvCtjTgpF_v6qRklJXquKvFd9DcFVcTCuI3xJMeD_zhb4yg3VEGU_UI1qPqlHeRqUNczcPHvmeLzK_FwADDg4dqbrjVsebfJxPcrzm6WRvMALd5dqyR75-2MvgXC4ysF0rgSz5G2b6C5hopNoR3h8ZZW3zE0CMaWgXuDwWY9YJihwTOl5rMv39RFQvAh646VI6D-JmK62l_tQgY5FaTqk0L7NxF61JbymGltuOhyJiajf3tWplXnRDsUj24hEEKqErP7gtOqETWP7L5AKRpLXb4VHj-p66BUwWz_i3sKpndLDkWR_TtBMKipr1KQZCsim5SUe6XZzX81qzV-3mqzbd9S3OPAPxf2xue227HQY1uO1D4u0gIGcpeEIhBPERA3nJcGlmUSgQPU0yGinnwxR3VIG6n7naXw9O04Xu9uMobU3uChondGThyuqvfV3Rp6baUdfJD3s8O","summary":[{"type":"summary_text","text":"**Reviewing quickstart integration seam**"}]},{"id":"fc_017bff223290b7ba016a58b1760e70819abcd7f6c935d37363","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading quickstart integration seam\",\"path\":\"crates/spt/tests/quickstart_e2e.rs\",\"selector\":\"1-145\"}","call_id":"call_Pu1uyqaBAo7HtfdDHDTcaTYj","name":"read"},{"id":"fc_017bff223290b7ba016a58b1760e80819a817cecc47106bffb","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading ready receive implementation\",\"path\":\"crates/spt-msg/src/ready.rs\",\"selector\":\"35-215\"}","call_id":"call_A5BoSnoEmfEbw5dAcbk30PT8","name":"read"},{"id":"fc_017bff223290b7ba016a58b1760e84819a80eac76c9c11ec6c","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding id validation hyphens\",\"pattern\":\"validate_endpoint_id|INVALID_ID|ASCII|hyphen|'-'\",\"path\":\"crates/spt-proto/src/id.rs; crates/spt/src/cli.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_Cpwv3TEFNFJFSmZuZwnqEZIK","name":"grep"},{"id":"fc_017bff223290b7ba016a58b1760e8c819ab1f5d47200959cfe","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading endpoint id grammar\",\"path\":\"crates/spt-proto/src/id.rs\",\"selector\":\"1-180\"}","call_id":"call_oeQfuiR0TWTRCt6Bcr3LVSYe","name":"read"}]},"duration":6688.891499999911,"ttft":2625.330299999565,"contextSnapshot":{"promptTokens":195426,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Pu1uyqaBAo7HtfdDHDTcaTYj|fc_017bff223290b7ba016a58b1760e70819abcd7f6c935d37363","toolName":"read","startedAt":"2026-07-16T10:24:54.168Z","args":{"path":"crates/spt/tests/quickstart_e2e.rs"},"intent":"Reading quickstart integration seam"},"id":"8213e59b","parentId":"40a66d0e","timestamp":"2026-07-16T10:24:54.168Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_A5BoSnoEmfEbw5dAcbk30PT8|fc_017bff223290b7ba016a58b1760e80819a817cecc47106bffb","toolName":"read","startedAt":"2026-07-16T10:24:54.168Z","args":{"path":"crates/spt-msg/src/ready.rs"},"intent":"Reading ready receive implementation"},"id":"1ea5092e","parentId":"8213e59b","timestamp":"2026-07-16T10:24:54.168Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Cpwv3TEFNFJFSmZuZwnqEZIK|fc_017bff223290b7ba016a58b1760e84819a80eac76c9c11ec6c","toolName":"grep","startedAt":"2026-07-16T10:24:54.168Z","args":{"path":"crates/spt-proto/src/id.rs; crates/spt/src/cli.rs"},"intent":"Finding id validation hyphens"},"id":"b7bb0e2f","parentId":"1ea5092e","timestamp":"2026-07-16T10:24:54.168Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_oeQfuiR0TWTRCt6Bcr3LVSYe|fc_017bff223290b7ba016a58b1760e8c819ab1f5d47200959cfe","toolName":"read","startedAt":"2026-07-16T10:24:54.168Z","args":{"path":"crates/spt-proto/src/id.rs"},"intent":"Reading endpoint id grammar"},"id":"314705ab","parentId":"b7bb0e2f","timestamp":"2026-07-16T10:24:54.168Z"}
{"type":"message","id":"bc207ad0","parentId":"314705ab","timestamp":"2026-07-16T10:24:54.174Z","message":{"role":"toolResult","toolCallId":"call_Pu1uyqaBAo7HtfdDHDTcaTYj|fc_017bff223290b7ba016a58b1760e70819abcd7f6c935d37363","toolName":"read","content":[{"type":"text","text":"//! M6-D4 quickstart rung (rewritten M7 D5) — the published human quickstart\n//! (docs-site/src/quickstart/messaging.md) executed step for step against the\n//! real binary: the `how-to` topics the agent prompt blocks point at name the\n//! commands the guide relies on → ready brings sergey online → live send\n//! delivers (`SENT`) and the `<EVENT type=\"msg\" from=…>` envelope lands on\n//! sergey's stdout → sergey goes away → send spools (`QUEUED`) → `ready --once` drains\n//! the backlog. If the documented flow stops working, this fails — the doc\n//! cannot silently rot (REQ-DOCS-2's \"runnable, deterministic\" promise,\n//! CI-run on both platforms). The pairing `<details>` section is two-machine\n//! by nature — its evidence is the twohost ladder's join rung, not this test.\n//!\n//! Hermetic: isolated SPT_HOME per run; not env-gated (cheap, local-only).\n//\n// [int->REQ-DOCS-2]\n\nuse std::io::{BufRead, BufReader, Read};\nuse std::process::{Command, Stdio};\nuse std::time::Duration;\n\nmod common;\nuse common::CommandNoWindowExt;\n\nfn spt() -> Command {\n    let mut c = Command::new(env!(\"CARGO_BIN_EXE_spt\"));\n    c.no_window();\n    c\n}\n\n#[test]\nfn published_messaging_quickstart_runs_as_written() {\n    let home = tempfile::tempdir().expect(\"tempdir\");\n    let home = home.path();\n\n    // ---- steps 3/4 preamble: the agent prompt blocks say \"run `spt how-to\n    // <topic>` and follow it\" — assert the in-binary guidance actually names\n    // the commands the guide's captured outputs rely on (REQ-DOCS-6: the\n    // binary is the single source; if these texts rot, the quickstart's\n    // prompts mislead every agent that follows them).\n    // [int->REQ-DOCS-6]\n    let topics = run_ok(home, &[\"how-to\"]);\n    assert!(\n        topics.contains(\"ready\"),\n        \"topic list names ready: {topics:?}\"\n    );\n    assert!(topics.contains(\"send\"), \"topic list names send: {topics:?}\");\n    let ready_text = run_ok(home, &[\"how-to\", \"ready\"]);\n    assert!(\n        ready_text.contains(\"spt ready <id>\"),\n        \"ready how-to names the command\"\n    );\n    assert!(\n        ready_text.contains(\"--once\"),\n        \"ready how-to teaches the one-shot fallback\"\n    );\n    assert!(\n        ready_text.contains(\"<EVENT type=\"),\n        \"ready how-to explains the EVENT envelope delivery shape\"\n    );\n    let send_text = run_ok(home, &[\"how-to\", \"send\"]);\n    assert!(\n        send_text.contains(\"spt send <target>\"),\n        \"send how-to names the command\"\n    );\n    assert!(\n        send_text.contains(\"QUEUED\"),\n        \"send how-to explains SENT vs QUEUED\"\n    );\n\n    // ---- step 3: the receiver agent comes online (`spt ready sergey`) ----\n    let mut sergey = spt()\n        .args([\"ready\", \"sergey\"])\n        .env(\"SPT_HOME\", home)\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped())\n        .spawn()\n        .expect(\"spawn ready sergey\");\n    // READY:sergey is the online signal (stderr).\n    let mut sergey_err = BufReader::new(sergey.stderr.take().unwrap());\n    let mut line = String::new();\n    sergey_err.read_line(&mut line).expect(\"read READY line\");\n    assert!(\n        line.contains(\"READY:sergey\"),\n        \"expected READY:sergey, got {line:?}\"\n    );\n\n    // ---- step 4: live send from lea ----\n    let out = send(\n        home,\n        &[\"send\", \"sergey\", \"--from\", \"lea\"],\n        \"hello sergey - lea here\",\n    );\n    assert!(\n        out.contains(\"SENT:sergey\"),\n        \"live delivery must report SENT:sergey, got {out:?}\"\n    );\n    // The documented receive shape: one EVENT envelope line on sergey's\n    // stdout — from attribute carries the sender, body rides inside.\n    // [int->REQ-MSG-4]\n    let mut sergey_out = BufReader::new(sergey.stdout.take().unwrap());\n    let mut l1 = String::new();\n    sergey_out.read_line(&mut l1).expect(\"read envelope line\");\n    assert!(\n        l1.contains(r#\"<EVENT type=\"msg\" from=\"lea\">hello sergey - lea here</EVENT>\"#),\n        \"envelope line, got {l1:?}\"\n    );\n\n    // ---- step 5: sergey goes away; the send spools ----\n    sergey.kill().expect(\"stop sergey\");\n    let _ = sergey.wait();\n    let out = send(\n        home,\n        &[\"send\", \"sergey\", \"--from\", \"lea\"],\n        \"ping while you were away\",\n    );\n    assert!(\n        out.contains(\"QUEUED:sergey\"),\n        \"offline delivery must report QUEUED:sergey, got {out:?}\"\n    );\n\n    // ---- sergey comes back: the backlog drains (`ready --once`) ----\n    let drained = spt()\n        .args([\"ready\", \"sergey\", \"--once\"])\n        .env(\"SPT_HOME\", home)\n        .output()\n        .expect(\"ready --once\");\n    let stdout = String::from_utf8_lossy(&drained.stdout);\n    // The backlog path renders the same envelope shape as the live path.\n    // [int->REQ-MSG-4]\n    assert!(\n        stdout.contains(r#\"<EVENT type=\"msg\" from=\"lea\">ping while you were away</EVENT>\"#),\n        \"backlog must drain enveloped on return, got {stdout:?}\"\n    );\n}\n\n/// Run a one-shot spt command expecting exit 0; return its stdout.\nfn run_ok(home: &std::path::Path, args: &[&str]) -> String {\n    let out = spt()\n        .args(args)\n        .env(\"SPT_HOME\", home)\n        .output()\n        .expect(\"spt runs\");\n    assert!(\n        out.status.success(),\n        \"`spt {}` failed: {:?}\",\n        args.join(\" \"),\n        out\n    );\n    String::from_utf8_lossy(&out.stdout).into_owned()\n}\n\n[Showing lines 1-148 of 187. Use :149 to continue]"}],"details":{"truncation":{"content":"//! M6-D4 quickstart rung (rewritten M7 D5) — the published human quickstart\r\n//! (docs-site/src/quickstart/messaging.md) executed step for step against the\r\n//! real binary: the `how-to` topics the agent prompt blocks point at name the\r\n//! commands the guide relies on → ready brings sergey online → live send\r\n//! delivers (`SENT`) and the `<EVENT type=\"msg\" from=…>` envelope lands on\r\n//! sergey's stdout → sergey goes away → send spools (`QUEUED`) → `ready --once` drains\r\n//! the backlog. If the documented flow stops working, this fails — the doc\r\n//! cannot silently rot (REQ-DOCS-2's \"runnable, deterministic\" promise,\r\n//! CI-run on both platforms). The pairing `<details>` section is two-machine\r\n//! by nature — its evidence is the twohost ladder's join rung, not this test.\r\n//!\r\n//! Hermetic: isolated SPT_HOME per run; not env-gated (cheap, local-only).\r\n//\r\n// [int->REQ-DOCS-2]\r\n\r\nuse std::io::{BufRead, BufReader, Read};\r\nuse std::process::{Command, Stdio};\r\nuse std::time::Duration;\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\r\n\r\nfn spt() -> Command {\r\n    let mut c = Command::new(env!(\"CARGO_BIN_EXE_spt\"));\r\n    c.no_window();\r\n    c\r\n}\r\n\r\n#[test]\r\nfn published_messaging_quickstart_runs_as_written() {\r\n    let home = tempfile::tempdir().expect(\"tempdir\");\r\n    let home = home.path();\r\n\r\n    // ---- steps 3/4 preamble: the agent prompt blocks say \"run `spt how-to\r\n    // <topic>` and follow it\" — assert the in-binary guidance actually names\r\n    // the commands the guide's captured outputs rely on (REQ-DOCS-6: the\r\n    // binary is the single source; if these texts rot, the quickstart's\r\n    // prompts mislead every agent that follows them).\r\n    // [int->REQ-DOCS-6]\r\n    let topics = run_ok(home, &[\"how-to\"]);\r\n    assert!(\r\n        topics.contains(\"ready\"),\r\n        \"topic list names ready: {topics:?}\"\r\n    );\r\n    assert!(topics.contains(\"send\"), \"topic list names send: {topics:?}\");\r\n    let ready_text = run_ok(home, &[\"how-to\", \"ready\"]);\r\n    assert!(\r\n        ready_text.contains(\"spt ready <id>\"),\r\n        \"ready how-to names the command\"\r\n    );\r\n    assert!(\r\n        ready_text.contains(\"--once\"),\r\n        \"ready how-to teaches the one-shot fallback\"\r\n    );\r\n    assert!(\r\n        ready_text.contains(\"<EVENT type=\"),\r\n        \"ready how-to explains the EVENT envelope delivery shape\"\r\n    );\r\n    let send_text = run_ok(home, &[\"how-to\", \"send\"]);\r\n    assert!(\r\n        send_text.contains(\"spt send <target>\"),\r\n        \"send how-to names the command\"\r\n    );\r\n    assert!(\r\n        send_text.contains(\"QUEUED\"),\r\n        \"send how-to explains SENT vs QUEUED\"\r\n    );\r\n\r\n    // ---- step 3: the receiver agent comes online (`spt ready sergey`) ----\r\n    let mut sergey = spt()\r\n        .args([\"ready\", \"sergey\"])\r\n        .env(\"SPT_HOME\", home)\r\n        .stdout(Stdio::piped())\r\n        .stderr(Stdio::piped())\r\n        .spawn()\r\n        .expect(\"spawn ready sergey\");\r\n    // READY:sergey is the online signal (stderr).\r\n    let mut sergey_err = BufReader::new(sergey.stderr.take().unwrap());\r\n    let mut line = String::new();\r\n    sergey_err.read_line(&mut line).expect(\"read READY line\");\r\n    assert!(\r\n        line.contains(\"READY:sergey\"),\r\n        \"expected READY:sergey, got {line:?}\"\r\n    );\r\n\r\n    // ---- step 4: live send from lea ----\r\n    let out = send(\r\n        home,\r\n        &[\"send\", \"sergey\", \"--from\", \"lea\"],\r\n        \"hello sergey - lea here\",\r\n    );\r\n    assert!(\r\n        out.contains(\"SENT:sergey\"),\r\n        \"live delivery must report SENT:sergey, got {out:?}\"\r\n    );\r\n    // The documented receive shape: one EVENT envelope line on sergey's\r\n    // stdout — from attribute carries the sender, body rides inside.\r\n    // [int->REQ-MSG-4]\r\n    let mut sergey_out = BufReader::new(sergey.stdout.take().unwrap());\r\n    let mut l1 = String::new();\r\n    sergey_out.read_line(&mut l1).expect(\"read envelope line\");\r\n    assert!(\r\n        l1.contains(r#\"<EVENT type=\"msg\" from=\"lea\">hello sergey - lea here</EVENT>\"#),\r\n        \"envelope line, got {l1:?}\"\r\n    );\r\n\r\n    // ---- step 5: sergey goes away; the send spools ----\r\n    sergey.kill().expect(\"stop sergey\");\r\n    let _ = sergey.wait();\r\n    let out = send(\r\n        home,\r\n        &[\"send\", \"sergey\", \"--from\", \"lea\"],\r\n        \"ping while you were away\",\r\n    );\r\n    assert!(\r\n        out.contains(\"QUEUED:sergey\"),\r\n        \"offline delivery must report QUEUED:sergey, got {out:?}\"\r\n    );\r\n\r\n    // ---- sergey comes back: the backlog drains (`ready --once`) ----\r\n    let drained = spt()\r\n        .args([\"ready\", \"sergey\", \"--once\"])\r\n        .env(\"SPT_HOME\", home)\r\n        .output()\r\n        .expect(\"ready --once\");\r\n    let stdout = String::from_utf8_lossy(&drained.stdout);\r\n    // The backlog path renders the same envelope shape as the live path.\r\n    // [int->REQ-MSG-4]\r\n    assert!(\r\n        stdout.contains(r#\"<EVENT type=\"msg\" from=\"lea\">ping while you were away</EVENT>\"#),\r\n        \"backlog must drain enveloped on return, got {stdout:?}\"\r\n    );\r\n}\r\n\r\n/// Run a one-shot spt command expecting exit 0; return its stdout.\r\nfn run_ok(home: &std::path::Path, args: &[&str]) -> String {\r\n    let out = spt()\r\n        .args(args)\r\n        .env(\"SPT_HOME\", home)\r\n        .output()\r\n        .expect(\"spt runs\");\r\n    assert!(\r\n        out.status.success(),\r\n        \"`spt {}` failed: {:?}\",\r\n        args.join(\" \"),\r\n        out\r\n    );\r\n    String::from_utf8_lossy(&out.stdout).into_owned()\r","truncated":true,"truncatedBy":"lines","totalLines":187,"totalBytes":5468,"outputLines":148,"outputBytes":5468,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! M6-D4 quickstart rung (rewritten M7 D5) — the published human quickstart\r\n//! (docs-site/src/quickstart/messaging.md) executed step for step against the\r\n//! real binary: the `how-to` topics the agent prompt blocks point at name the\r\n//! commands the guide relies on → ready brings sergey online → live send\r\n//! delivers (`SENT`) and the `<EVENT type=\"msg\" from=…>` envelope lands on\r\n//! sergey's stdout → sergey goes away → send spools (`QUEUED`) → `ready --once` drains\r\n//! the backlog. If the documented flow stops working, this fails — the doc\r\n//! cannot silently rot (REQ-DOCS-2's \"runnable, deterministic\" promise,\r\n//! CI-run on both platforms). The pairing `<details>` section is two-machine\r\n//! by nature — its evidence is the twohost ladder's join rung, not this test.\r\n//!\r\n//! Hermetic: isolated SPT_HOME per run; not env-gated (cheap, local-only).\r\n//\r\n// [int->REQ-DOCS-2]\r\n\r\nuse std::io::{BufRead, BufReader, Read};\r\nuse std::process::{Command, Stdio};\r\nuse std::time::Duration;\r\n\r\nmod common;\r\nuse common::CommandNoWindowExt;\r\n\r\nfn spt() -> Command {\r\n    let mut c = Command::new(env!(\"CARGO_BIN_EXE_spt\"));\r\n    c.no_window();\r\n    c\r\n}\r\n\r\n#[test]\r\nfn published_messaging_quickstart_runs_as_written() {\r\n    let home = tempfile::tempdir().expect(\"tempdir\");\r\n    let home = home.path();\r\n\r\n    // ---- steps 3/4 preamble: the agent prompt blocks say \"run `spt how-to\r\n    // <topic>` and follow it\" — assert the in-binary guidance actually names\r\n    // the commands the guide's captured outputs rely on (REQ-DOCS-6: the\r\n    // binary is the single source; if these texts rot, the quickstart's\r\n    // prompts mislead every agent that follows them).\r\n    // [int->REQ-DOCS-6]\r\n    let topics = run_ok(home, &[\"how-to\"]);\r\n    assert!(\r\n        topics.contains(\"ready\"),\r\n        \"topic list names ready: {topics:?}\"\r\n    );\r\n    assert!(topics.contains(\"send\"), \"topic list names send: {topics:?}\");\r\n    let ready_text = run_ok(home, &[\"how-to\", \"ready\"]);\r\n    assert!(\r\n        ready_text.contains(\"spt ready <id>\"),\r\n        \"ready how-to names the command\"\r\n    );\r\n    assert!(\r\n        ready_text.contains(\"--once\"),\r\n        \"ready how-to teaches the one-shot fallback\"\r\n    );\r\n    assert!(\r\n        ready_text.contains(\"<EVENT type=\"),\r\n        \"ready how-to explains the EVENT envelope delivery shape\"\r\n    );\r\n    let send_text = run_ok(home, &[\"how-to\", \"send\"]);\r\n    assert!(\r\n        send_text.contains(\"spt send <target>\"),\r\n        \"send how-to names the command\"\r\n    );\r\n    assert!(\r\n        send_text.contains(\"QUEUED\"),\r\n        \"send how-to explains SENT vs QUEUED\"\r\n    );\r\n\r\n    // ---- step 3: the receiver agent comes online (`spt ready sergey`) ----\r\n    let mut sergey = spt()\r\n        .args([\"ready\", \"sergey\"])\r\n        .env(\"SPT_HOME\", home)\r\n        .stdout(Stdio::piped())\r\n        .stderr(Stdio::piped())\r\n        .spawn()\r\n        .expect(\"spawn ready sergey\");\r\n    // READY:sergey is the online signal (stderr).\r\n    let mut sergey_err = BufReader::new(sergey.stderr.take().unwrap());\r\n    let mut line = String::new();\r\n    sergey_err.read_line(&mut line).expect(\"read READY line\");\r\n    assert!(\r\n        line.contains(\"READY:sergey\"),\r\n        \"expected READY:sergey, got {line:?}\"\r\n    );\r\n\r\n    // ---- step 4: live send from lea ----\r\n    let out = send(\r\n        home,\r\n        &[\"send\", \"sergey\", \"--from\", \"lea\"],\r\n        \"hello sergey - lea here\",\r\n    );\r\n    assert!(\r\n        out.contains(\"SENT:sergey\"),\r\n        \"live delivery must report SENT:sergey, got {out:?}\"\r\n    );\r\n    // The documented receive shape: one EVENT envelope line on sergey's\r\n    // stdout — from attribute carries the sender, body rides inside.\r\n    // [int->REQ-MSG-4]\r\n    let mut sergey_out = BufReader::new(sergey.stdout.take().unwrap());\r\n    let mut l1 = String::new();\r\n    sergey_out.read_line(&mut l1).expect(\"read envelope line\");\r\n    assert!(\r\n        l1.contains(r#\"<EVENT type=\"msg\" from=\"lea\">hello sergey - lea here</EVENT>\"#),\r\n        \"envelope line, got {l1:?}\"\r\n    );\r\n\r\n    // ---- step 5: sergey goes away; the send spools ----\r\n    sergey.kill().expect(\"stop sergey\");\r\n    let _ = sergey.wait();\r\n    let out = send(\r\n        home,\r\n        &[\"send\", \"sergey\", \"--from\", \"lea\"],\r\n        \"ping while you were away\",\r\n    );\r\n    assert!(\r\n        out.contains(\"QUEUED:sergey\"),\r\n        \"offline delivery must report QUEUED:sergey, got {out:?}\"\r\n    );\r\n\r\n    // ---- sergey comes back: the backlog drains (`ready --once`) ----\r\n    let drained = spt()\r\n        .args([\"ready\", \"sergey\", \"--once\"])\r\n        .env(\"SPT_HOME\", home)\r\n        .output()\r\n        .expect(\"ready --once\");\r\n    let stdout = String::from_utf8_lossy(&drained.stdout);\r\n    // The backlog path renders the same envelope shape as the live path.\r\n    // [int->REQ-MSG-4]\r\n    assert!(\r\n        stdout.contains(r#\"<EVENT type=\"msg\" from=\"lea\">ping while you were away</EVENT>\"#),\r\n        \"backlog must drain enveloped on return, got {stdout:?}\"\r\n    );\r\n}\r\n\r\n/// Run a one-shot spt command expecting exit 0; return its stdout.\r\nfn run_ok(home: &std::path::Path, args: &[&str]) -> String {\r\n    let out = spt()\r\n        .args(args)\r\n        .env(\"SPT_HOME\", home)\r\n        .output()\r\n        .expect(\"spt runs\");\r\n    assert!(\r\n        out.status.success(),\r\n        \"`spt {}` failed: {:?}\",\r\n        args.join(\" \"),\r\n        out\r\n    );\r\n    String::from_utf8_lossy(&out.stdout).into_owned()\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\quickstart_e2e.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":187,"totalBytes":5468,"outputLines":148,"outputBytes":5468,"shownRange":{"start":1,"end":148},"nextOffset":149}}},"isError":false,"timestamp":1784197494174}}
{"type":"message","id":"c72bd719","parentId":"bc207ad0","timestamp":"2026-07-16T10:24:54.178Z","message":{"role":"toolResult","toolCallId":"call_A5BoSnoEmfEbw5dAcbk30PT8|fc_017bff223290b7ba016a58b1760e80819a817cecc47106bffb","toolName":"read","content":[{"type":"text","text":"const RECV_POLL: Duration = Duration::from_millis(200);\n\n/// A running ready agent: an owned perch + a registered loopback listener.\npub struct ReadyAgent {\n    id: String,\n    listener: PollListener,\n    perch_path: PathBuf,\n    closed: bool,\n}\n\nimpl ReadyAgent {\n    /// Register the perch (info.json + ready marker + listener address) and\n    /// drain the offline backlog. Returns the agent and any backlog messages\n    /// (oldest first) that were spooled while it was down.\n    // [impl->REQ-MSG-3]\n    pub fn start(id: &str) -> Result<(ReadyAgent, Vec<(String, String)>), String> {\n        Self::start_homed(id, None)\n    }\n\n    /// [`start`](Self::start) with an explicit home-subnet pick for a NEW\n    /// endpoint (D9-5, REQ-INST-15): home is assigned at creation through the\n    /// shared seam ([`spt_store::home::stamp_creation_fields`]) — auto on a\n    /// sole-subnet node, `subnet` required on a multi-subnet one\n    /// (refuse-and-qualify), local-only when unpaired. A restart carries the\n    /// durable creation-time fields forward (home is immutable; the resource\n    /// blurb and knobs are user state a re-register must not wipe).\n    // [impl->REQ-INST-15]\n    pub fn start_homed(\n        id: &str,\n        subnet: Option<&str>,\n    ) -> Result<(ReadyAgent, Vec<(String, String)>), String> {\n        // [impl->REQ-HAZARD-ID-CHARSET] guard the id charset at the creation seam.\n        spt_proto::id::validate_endpoint_id(id).map_err(|e| e.to_string())?;\n        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n        fs::create_dir_all(&perch_path)\n            .map_err(|e| format!(\"Failed to create perch dir: {}\", e))?;\n\n        // info.json first, so the record exists by the time the address is live.\n        let prior = info::read_info(&perch_path);\n        let sid = session_id();\n        let mut rec = InfoJson::new(\n            id,\n            &now_stamp(),\n            std::process::id(),\n            &sid,\n            \"ready_agent\",\n        );\n        let subnets = spt_store::subnet::SubnetStore::load();\n        let mut vis = spt_store::visibility::VisibilityStore::load();\n        // Nested-resolution seam (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): a NESTED id\n        // derives its home from the PARENT perch, so a child-side ready verb needs no\n        // `--subnet` the child can't know. Explicit still wins; a Self id falls through\n        // to assign_home's auto/refuse policy unchanged. The impure parent read lives\n        // here at the caller boundary, keeping stamp_creation_fields pure.\n        // [impl->REQ-PSYCHE-NESTED-RESOLUTION]\n        let eff_subnet = spt_store::home::effective_subnet(id, subnet, &perch::owlery_dir());\n        let dirty = spt_store::home::stamp_creation_fields(\n            &mut rec,\n            prior.as_ref(),\n            &subnets,\n            &mut vis,\n            eff_subnet.as_deref(),\n            None,\n        )\n        .map_err(|e| format!(\"HOME: {e}\"))?;\n        if dirty {\n            let _ = vis.save();\n        }\n        info::write_info(&perch_path, &rec)\n            .map_err(|e| format!(\"Failed to write info.json: {}\", e))?;\n\n        // ready marker — the liveness signal senders check.\n        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\n            .map_err(|e| format!(\"Failed to write ready marker: {}\", e))?;\n\n        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind\n        // writes info.json directly and — unlike the live `establish_perch` path\n        // (startup.rs:250) — never ledgered, so an offline ready perch carried\n        // ZERO session rows and the picker's offline Resume-from-history (which\n        // gates on ledger rows) never offered it. Append a Boot row here, mirroring\n        // the live path: the boot row dedups against a same-session re-bind, an\n        // empty session is not ledgered, and a ledger problem must never fail the\n        // bind (best-effort).\n        // [impl->REQ-READY-AGENT-RESUME]\n        if !sid.is_empty() {\n            let _ = spt_store::sessions::append(\n                &perch_path,\n                &spt_store::sessions::SessionEntry {\n                    ts: spt_store::timefmt::now_rfc3339(),\n                    session_id: sid.clone(),\n                    trigger: spt_store::sessions::SessionTrigger::Boot,\n                    // Mirror info.cwd if the ready bind tracked one (REQ-SESSION-RESUME-TEMPLATE).\n                    cwd: rec.cwd.clone(),\n                    // `append` stamps the persisted monotonic ordinal (REQ-DIGEST-CURSOR).\n                    ordinal: None,\n                    // Record the adapter this ready session runs under so a later\n                    // Resume restores the same harness (D-2, REQ-SESSION-ADAPTER-RECORDED).\n                    // [impl->REQ-SESSION-ADAPTER-RECORDED]\n                    adapter: rec.adapter.clone(),\n                },\n            );\n            // New session/cwd → refresh this endpoint's project-index row\n            // (+ the shared cwd cache) — ADR-0037 §3. Fire-and-forget.\n            // [impl->REQ-PROJECT-INDEX-INVALIDATION]\n            spt_store::projinval::nudge(&spt_store::projinval::InvalScope::Endpoint {\n                id: id.to_string(),\n                cwd: rec.cwd.clone(),\n            });\n        }\n\n        // Bind + register the listener address. Blocking with a short poll so\n        // recv() stays responsive.\n        let listener = PollListener::bind_blocking(id, RECV_POLL)?;\n\n        // Drain the offline backlog (non-deferred only; deferred rows survive\n        // for the hook channel). Carried structurally as (from, body) — the\n        // delivery edge composes the <EVENT> (ADR-0020).\n        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg (ready-agent listener startup).\n        let audit =\n            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\n        let backlog: Vec<(String, String)> = spool::drain_non_deferred_audited_at(&perch_path, &audit)\n            .map_err(|e| format!(\"Failed to drain spool backlog: {}\", e))?\n            .into_iter()\n            .map(|m| (m.from, m.body))\n            .collect();\n\n        Ok((\n            ReadyAgent {\n                id: id.to_string(),\n                listener,\n                perch_path,\n                closed: false,\n            },\n            backlog,\n        ))\n    }\n\n    /// The agent's id.\n    pub fn id(&self) -> &str {\n        &self.id\n    }\n\n    /// Wait up to one poll window for a live TCP message, returning the\n    /// structural `(from, body)`. Returns `None` on timeout (call again to keep\n    /// listening).\n    pub fn recv(&self) -> Option<(String, String)> {\n        self.listener.check_message_blocking()\n    }\n\n    /// Drain any non-deferred spool rows that accrued since startup (e.g. a\n    /// sender that spooled because it raced the listener bind, or a hook-channel\n    /// arrival), as `(from, body)`. Oldest first.\n    pub fn drain_spool(&self) -> Vec<(String, String)> {\n        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg (ready-agent since-startup drain).\n        let audit =\n            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\n        spool::drain_non_deferred_audited_at(&self.perch_path, &audit)\n            .unwrap_or_default()\n            .into_iter()\n            .map(|m| (m.from, m.body))\n            .collect()\n    }\n\n    /// Soft teardown (6.2): remove the `ready` marker and unregister the\n    /// address. The spool and `info.json` are **preserved** so a later restart\n    /// resumes its backlog. Idempotent.\n    // [impl->REQ-MSG-3]\n    // [impl->REQ-HAZARD-SOFT-CLEANUP]\n    pub fn soft_cleanup(&mut self) {\n        let _ = fs::remove_file(perch::resolve_ready_file(&self.id, ParentHint::Infer));\n        self.listener.close();\n        self.closed = true;\n    }\n}\n\nimpl Drop for ReadyAgent {\n    fn drop(&mut self) {\n        if !self.closed {\n            self.soft_cleanup();\n        }\n    }\n}\n\n/// Coarse creation timestamp (epoch seconds as a string). Opaque to readers —\n/// `info.json` treats `started` as a producer-format string.\n\n[Showing lines 34-218 of 370. Use :219 to continue]"}],"details":{"truncation":{"content":"const RECV_POLL: Duration = Duration::from_millis(200);\r\n\r\n/// A running ready agent: an owned perch + a registered loopback listener.\r\npub struct ReadyAgent {\r\n    id: String,\r\n    listener: PollListener,\r\n    perch_path: PathBuf,\r\n    closed: bool,\r\n}\r\n\r\nimpl ReadyAgent {\r\n    /// Register the perch (info.json + ready marker + listener address) and\r\n    /// drain the offline backlog. Returns the agent and any backlog messages\r\n    /// (oldest first) that were spooled while it was down.\r\n    // [impl->REQ-MSG-3]\r\n    pub fn start(id: &str) -> Result<(ReadyAgent, Vec<(String, String)>), String> {\r\n        Self::start_homed(id, None)\r\n    }\r\n\r\n    /// [`start`](Self::start) with an explicit home-subnet pick for a NEW\r\n    /// endpoint (D9-5, REQ-INST-15): home is assigned at creation through the\r\n    /// shared seam ([`spt_store::home::stamp_creation_fields`]) — auto on a\r\n    /// sole-subnet node, `subnet` required on a multi-subnet one\r\n    /// (refuse-and-qualify), local-only when unpaired. A restart carries the\r\n    /// durable creation-time fields forward (home is immutable; the resource\r\n    /// blurb and knobs are user state a re-register must not wipe).\r\n    // [impl->REQ-INST-15]\r\n    pub fn start_homed(\r\n        id: &str,\r\n        subnet: Option<&str>,\r\n    ) -> Result<(ReadyAgent, Vec<(String, String)>), String> {\r\n        // [impl->REQ-HAZARD-ID-CHARSET] guard the id charset at the creation seam.\r\n        spt_proto::id::validate_endpoint_id(id).map_err(|e| e.to_string())?;\r\n        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\r\n        fs::create_dir_all(&perch_path)\r\n            .map_err(|e| format!(\"Failed to create perch dir: {}\", e))?;\r\n\r\n        // info.json first, so the record exists by the time the address is live.\r\n        let prior = info::read_info(&perch_path);\r\n        let sid = session_id();\r\n        let mut rec = InfoJson::new(\r\n            id,\r\n            &now_stamp(),\r\n            std::process::id(),\r\n            &sid,\r\n            \"ready_agent\",\r\n        );\r\n        let subnets = spt_store::subnet::SubnetStore::load();\r\n        let mut vis = spt_store::visibility::VisibilityStore::load();\r\n        // Nested-resolution seam (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): a NESTED id\r\n        // derives its home from the PARENT perch, so a child-side ready verb needs no\r\n        // `--subnet` the child can't know. Explicit still wins; a Self id falls through\r\n        // to assign_home's auto/refuse policy unchanged. The impure parent read lives\r\n        // here at the caller boundary, keeping stamp_creation_fields pure.\r\n        // [impl->REQ-PSYCHE-NESTED-RESOLUTION]\r\n        let eff_subnet = spt_store::home::effective_subnet(id, subnet, &perch::owlery_dir());\r\n        let dirty = spt_store::home::stamp_creation_fields(\r\n            &mut rec,\r\n            prior.as_ref(),\r\n            &subnets,\r\n            &mut vis,\r\n            eff_subnet.as_deref(),\r\n            None,\r\n        )\r\n        .map_err(|e| format!(\"HOME: {e}\"))?;\r\n        if dirty {\r\n            let _ = vis.save();\r\n        }\r\n        info::write_info(&perch_path, &rec)\r\n            .map_err(|e| format!(\"Failed to write info.json: {}\", e))?;\r\n\r\n        // ready marker — the liveness signal senders check.\r\n        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\r\n            .map_err(|e| format!(\"Failed to write ready marker: {}\", e))?;\r\n\r\n        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind\r\n        // writes info.json directly and — unlike the live `establish_perch` path\r\n        // (startup.rs:250) — never ledgered, so an offline ready perch carried\r\n        // ZERO session rows and the picker's offline Resume-from-history (which\r\n        // gates on ledger rows) never offered it. Append a Boot row here, mirroring\r\n        // the live path: the boot row dedups against a same-session re-bind, an\r\n        // empty session is not ledgered, and a ledger problem must never fail the\r\n        // bind (best-effort).\r\n        // [impl->REQ-READY-AGENT-RESUME]\r\n        if !sid.is_empty() {\r\n            let _ = spt_store::sessions::append(\r\n                &perch_path,\r\n                &spt_store::sessions::SessionEntry {\r\n                    ts: spt_store::timefmt::now_rfc3339(),\r\n                    session_id: sid.clone(),\r\n                    trigger: spt_store::sessions::SessionTrigger::Boot,\r\n                    // Mirror info.cwd if the ready bind tracked one (REQ-SESSION-RESUME-TEMPLATE).\r\n                    cwd: rec.cwd.clone(),\r\n                    // `append` stamps the persisted monotonic ordinal (REQ-DIGEST-CURSOR).\r\n                    ordinal: None,\r\n                    // Record the adapter this ready session runs under so a later\r\n                    // Resume restores the same harness (D-2, REQ-SESSION-ADAPTER-RECORDED).\r\n                    // [impl->REQ-SESSION-ADAPTER-RECORDED]\r\n                    adapter: rec.adapter.clone(),\r\n                },\r\n            );\r\n            // New session/cwd → refresh this endpoint's project-index row\r\n            // (+ the shared cwd cache) — ADR-0037 §3. Fire-and-forget.\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: rec.cwd.clone(),\r\n            });\r\n        }\r\n\r\n        // Bind + register the listener address. Blocking with a short poll so\r\n        // recv() stays responsive.\r\n        let listener = PollListener::bind_blocking(id, RECV_POLL)?;\r\n\r\n        // Drain the offline backlog (non-deferred only; deferred rows survive\r\n        // for the hook channel). Carried structurally as (from, body) — the\r\n        // delivery edge composes the <EVENT> (ADR-0020).\r\n        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg (ready-agent listener startup).\r\n        let audit =\r\n            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\r\n        let backlog: Vec<(String, String)> = spool::drain_non_deferred_audited_at(&perch_path, &audit)\r\n            .map_err(|e| format!(\"Failed to drain spool backlog: {}\", e))?\r\n            .into_iter()\r\n            .map(|m| (m.from, m.body))\r\n            .collect();\r\n\r\n        Ok((\r\n            ReadyAgent {\r\n                id: id.to_string(),\r\n                listener,\r\n                perch_path,\r\n                closed: false,\r\n            },\r\n            backlog,\r\n        ))\r\n    }\r\n\r\n    /// The agent's id.\r\n    pub fn id(&self) -> &str {\r\n        &self.id\r\n    }\r\n\r\n    /// Wait up to one poll window for a live TCP message, returning the\r\n    /// structural `(from, body)`. Returns `None` on timeout (call again to keep\r\n    /// listening).\r\n    pub fn recv(&self) -> Option<(String, String)> {\r\n        self.listener.check_message_blocking()\r\n    }\r\n\r\n    /// Drain any non-deferred spool rows that accrued since startup (e.g. a\r\n    /// sender that spooled because it raced the listener bind, or a hook-channel\r\n    /// arrival), as `(from, body)`. Oldest first.\r\n    pub fn drain_spool(&self) -> Vec<(String, String)> {\r\n        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg (ready-agent since-startup drain).\r\n        let audit =\r\n            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\r\n        spool::drain_non_deferred_audited_at(&self.perch_path, &audit)\r\n            .unwrap_or_default()\r\n            .into_iter()\r\n            .map(|m| (m.from, m.body))\r\n            .collect()\r\n    }\r\n\r\n    /// Soft teardown (6.2): remove the `ready` marker and unregister the\r\n    /// address. The spool and `info.json` are **preserved** so a later restart\r\n    /// resumes its backlog. Idempotent.\r\n    // [impl->REQ-MSG-3]\r\n    // [impl->REQ-HAZARD-SOFT-CLEANUP]\r\n    pub fn soft_cleanup(&mut self) {\r\n        let _ = fs::remove_file(perch::resolve_ready_file(&self.id, ParentHint::Infer));\r\n        self.listener.close();\r\n        self.closed = true;\r\n    }\r\n}\r\n\r\nimpl Drop for ReadyAgent {\r\n    fn drop(&mut self) {\r\n        if !self.closed {\r\n            self.soft_cleanup();\r\n        }\r\n    }\r\n}\r\n\r\n/// Coarse creation timestamp (epoch seconds as a string). Opaque to readers —\r\n/// `info.json` treats `started` as a producer-format string.\r","truncated":true,"truncatedBy":"lines","totalLines":337,"totalBytes":8354,"outputLines":185,"outputBytes":8354,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"const RECV_POLL: Duration = Duration::from_millis(200);\r\n\r\n/// A running ready agent: an owned perch + a registered loopback listener.\r\npub struct ReadyAgent {\r\n    id: String,\r\n    listener: PollListener,\r\n    perch_path: PathBuf,\r\n    closed: bool,\r\n}\r\n\r\nimpl ReadyAgent {\r\n    /// Register the perch (info.json + ready marker + listener address) and\r\n    /// drain the offline backlog. Returns the agent and any backlog messages\r\n    /// (oldest first) that were spooled while it was down.\r\n    // [impl->REQ-MSG-3]\r\n    pub fn start(id: &str) -> Result<(ReadyAgent, Vec<(String, String)>), String> {\r\n        Self::start_homed(id, None)\r\n    }\r\n\r\n    /// [`start`](Self::start) with an explicit home-subnet pick for a NEW\r\n    /// endpoint (D9-5, REQ-INST-15): home is assigned at creation through the\r\n    /// shared seam ([`spt_store::home::stamp_creation_fields`]) — auto on a\r\n    /// sole-subnet node, `subnet` required on a multi-subnet one\r\n    /// (refuse-and-qualify), local-only when unpaired. A restart carries the\r\n    /// durable creation-time fields forward (home is immutable; the resource\r\n    /// blurb and knobs are user state a re-register must not wipe).\r\n    // [impl->REQ-INST-15]\r\n    pub fn start_homed(\r\n        id: &str,\r\n        subnet: Option<&str>,\r\n    ) -> Result<(ReadyAgent, Vec<(String, String)>), String> {\r\n        // [impl->REQ-HAZARD-ID-CHARSET] guard the id charset at the creation seam.\r\n        spt_proto::id::validate_endpoint_id(id).map_err(|e| e.to_string())?;\r\n        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\r\n        fs::create_dir_all(&perch_path)\r\n            .map_err(|e| format!(\"Failed to create perch dir: {}\", e))?;\r\n\r\n        // info.json first, so the record exists by the time the address is live.\r\n        let prior = info::read_info(&perch_path);\r\n        let sid = session_id();\r\n        let mut rec = InfoJson::new(\r\n            id,\r\n            &now_stamp(),\r\n            std::process::id(),\r\n            &sid,\r\n            \"ready_agent\",\r\n        );\r\n        let subnets = spt_store::subnet::SubnetStore::load();\r\n        let mut vis = spt_store::visibility::VisibilityStore::load();\r\n        // Nested-resolution seam (F-030 W4, REQ-PSYCHE-NESTED-RESOLUTION): a NESTED id\r\n        // derives its home from the PARENT perch, so a child-side ready verb needs no\r\n        // `--subnet` the child can't know. Explicit still wins; a Self id falls through\r\n        // to assign_home's auto/refuse policy unchanged. The impure parent read lives\r\n        // here at the caller boundary, keeping stamp_creation_fields pure.\r\n        // [impl->REQ-PSYCHE-NESTED-RESOLUTION]\r\n        let eff_subnet = spt_store::home::effective_subnet(id, subnet, &perch::owlery_dir());\r\n        let dirty = spt_store::home::stamp_creation_fields(\r\n            &mut rec,\r\n            prior.as_ref(),\r\n            &subnets,\r\n            &mut vis,\r\n            eff_subnet.as_deref(),\r\n            None,\r\n        )\r\n        .map_err(|e| format!(\"HOME: {e}\"))?;\r\n        if dirty {\r\n            let _ = vis.save();\r\n        }\r\n        info::write_info(&perch_path, &rec)\r\n            .map_err(|e| format!(\"Failed to write info.json: {}\", e))?;\r\n\r\n        // ready marker — the liveness signal senders check.\r\n        fs::write(perch::resolve_ready_file(id, ParentHint::Infer), \"\")\r\n            .map_err(|e| format!(\"Failed to write ready marker: {}\", e))?;\r\n\r\n        // Session ledger (REQ-READY-AGENT-RESUME): a harness-hosted ready bind\r\n        // writes info.json directly and — unlike the live `establish_perch` path\r\n        // (startup.rs:250) — never ledgered, so an offline ready perch carried\r\n        // ZERO session rows and the picker's offline Resume-from-history (which\r\n        // gates on ledger rows) never offered it. Append a Boot row here, mirroring\r\n        // the live path: the boot row dedups against a same-session re-bind, an\r\n        // empty session is not ledgered, and a ledger problem must never fail the\r\n        // bind (best-effort).\r\n        // [impl->REQ-READY-AGENT-RESUME]\r\n        if !sid.is_empty() {\r\n            let _ = spt_store::sessions::append(\r\n                &perch_path,\r\n                &spt_store::sessions::SessionEntry {\r\n                    ts: spt_store::timefmt::now_rfc3339(),\r\n                    session_id: sid.clone(),\r\n                    trigger: spt_store::sessions::SessionTrigger::Boot,\r\n                    // Mirror info.cwd if the ready bind tracked one (REQ-SESSION-RESUME-TEMPLATE).\r\n                    cwd: rec.cwd.clone(),\r\n                    // `append` stamps the persisted monotonic ordinal (REQ-DIGEST-CURSOR).\r\n                    ordinal: None,\r\n                    // Record the adapter this ready session runs under so a later\r\n                    // Resume restores the same harness (D-2, REQ-SESSION-ADAPTER-RECORDED).\r\n                    // [impl->REQ-SESSION-ADAPTER-RECORDED]\r\n                    adapter: rec.adapter.clone(),\r\n                },\r\n            );\r\n            // New session/cwd → refresh this endpoint's project-index row\r\n            // (+ the shared cwd cache) — ADR-0037 §3. Fire-and-forget.\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: rec.cwd.clone(),\r\n            });\r\n        }\r\n\r\n        // Bind + register the listener address. Blocking with a short poll so\r\n        // recv() stays responsive.\r\n        let listener = PollListener::bind_blocking(id, RECV_POLL)?;\r\n\r\n        // Drain the offline backlog (non-deferred only; deferred rows survive\r\n        // for the hook channel). Carried structurally as (from, body) — the\r\n        // delivery edge composes the <EVENT> (ADR-0020).\r\n        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg (ready-agent listener startup).\r\n        let audit =\r\n            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\r\n        let backlog: Vec<(String, String)> = spool::drain_non_deferred_audited_at(&perch_path, &audit)\r\n            .map_err(|e| format!(\"Failed to drain spool backlog: {}\", e))?\r\n            .into_iter()\r\n            .map(|m| (m.from, m.body))\r\n            .collect();\r\n\r\n        Ok((\r\n            ReadyAgent {\r\n                id: id.to_string(),\r\n                listener,\r\n                perch_path,\r\n                closed: false,\r\n            },\r\n            backlog,\r\n        ))\r\n    }\r\n\r\n    /// The agent's id.\r\n    pub fn id(&self) -> &str {\r\n        &self.id\r\n    }\r\n\r\n    /// Wait up to one poll window for a live TCP message, returning the\r\n    /// structural `(from, body)`. Returns `None` on timeout (call again to keep\r\n    /// listening).\r\n    pub fn recv(&self) -> Option<(String, String)> {\r\n        self.listener.check_message_blocking()\r\n    }\r\n\r\n    /// Drain any non-deferred spool rows that accrued since startup (e.g. a\r\n    /// sender that spooled because it raced the listener bind, or a hook-channel\r\n    /// arrival), as `(from, body)`. Oldest first.\r\n    pub fn drain_spool(&self) -> Vec<(String, String)> {\r\n        // [impl->REQ-SPOOL-TAKE-AUDIT] relay-backlog leg (ready-agent since-startup drain).\r\n        let audit =\r\n            spool::TakerAudit::new(spool::TakerLeg::RelayBacklog, None, Some(std::process::id()));\r\n        spool::drain_non_deferred_audited_at(&self.perch_path, &audit)\r\n            .unwrap_or_default()\r\n            .into_iter()\r\n            .map(|m| (m.from, m.body))\r\n            .collect()\r\n    }\r\n\r\n    /// Soft teardown (6.2): remove the `ready` marker and unregister the\r\n    /// address. The spool and `info.json` are **preserved** so a later restart\r\n    /// resumes its backlog. Idempotent.\r\n    // [impl->REQ-MSG-3]\r\n    // [impl->REQ-HAZARD-SOFT-CLEANUP]\r\n    pub fn soft_cleanup(&mut self) {\r\n        let _ = fs::remove_file(perch::resolve_ready_file(&self.id, ParentHint::Infer));\r\n        self.listener.close();\r\n        self.closed = true;\r\n    }\r\n}\r\n\r\nimpl Drop for ReadyAgent {\r\n    fn drop(&mut self) {\r\n        if !self.closed {\r\n            self.soft_cleanup();\r\n        }\r\n    }\r\n}\r\n\r\n/// Coarse creation timestamp (epoch seconds as a string). Opaque to readers —\r\n/// `info.json` treats `started` as a producer-format string.\r","startLine":34,"lineNumbers":[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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-msg\\src\\ready.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":370,"totalBytes":8354,"outputLines":185,"outputBytes":8354,"shownRange":{"start":34,"end":218},"nextOffset":219}}},"isError":false,"timestamp":1784197494177}}
{"type":"message","id":"8e1c7937","parentId":"c72bd719","timestamp":"2026-07-16T10:24:54.178Z","message":{"role":"toolResult","toolCallId":"call_oeQfuiR0TWTRCt6Bcr3LVSYe|fc_017bff223290b7ba016a58b1760e8c819ab1f5d47200959cfe","toolName":"read","content":[{"type":"text","text":"//! Endpoint-id charset contract — the addressable-name validator.\n//!\n//! Every addressable endpoint id/name is restricted to a deliberately narrow\n//! charset so that:\n//!   - `:` and `@` stay **reserved** as the future address delimiters of the\n//!     canonical qualified form `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) —\n//!     no bare id can ever contain them, so qualified-address parsing is\n//!     unambiguous and no permissive id-data accumulates that would need a\n//!     later migration;\n//!   - path separators, whitespace, and control characters cannot smuggle into\n//!     a perch directory name or a wire target string.\n//!\n//! Allowed: ASCII alphanumeric, `-`, `_`, and the Japanese script blocks\n//! (Hiragana, Katakana, CJK Unified Ideographs). Nothing else. The existing\n//! Psyche (`<parent>-psyche`) and Worker (`<parent>-w<N>`) suffix scheme uses\n//! only `-` + alphanumerics, so composite ids validate cleanly.\n// [impl->REQ-HAZARD-ID-CHARSET]\n\n/// Maximum endpoint-id length, in Unicode scalar values.\npub const MAX_ENDPOINT_ID_LEN: usize = 64;\n\n/// Why an endpoint id was rejected.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum IdError {\n    /// The id was the empty string.\n    Empty,\n    /// The id exceeded [`MAX_ENDPOINT_ID_LEN`] scalar values.\n    TooLong { len: usize, max: usize },\n    /// A disallowed character was found (with its scalar position).\n    BadChar { ch: char, pos: usize },\n}\n\nimpl std::fmt::Display for IdError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            IdError::Empty => write!(f, \"endpoint id must not be empty\"),\n            IdError::TooLong { len, max } => {\n                write!(f, \"endpoint id too long ({len} chars; max {max})\")\n            }\n            IdError::BadChar { ch, pos } => write!(\n                f,\n                \"endpoint id has a disallowed character {ch:?} at position {pos} \\\n                 (allowed: A-Z a-z 0-9 - _ and Hiragana/Katakana/Kanji; \\\n                 `:` and `@` are reserved address delimiters)\"\n            ),\n        }\n    }\n}\n\nimpl std::error::Error for IdError {}\n\n/// True if `c` is permitted in an endpoint id.\npub fn is_allowed_id_char(c: char) -> bool {\n    c.is_ascii_alphanumeric()\n        || c == '-'\n        || c == '_'\n        || matches!(c,\n            '\\u{3040}'..='\\u{309F}'   // Hiragana\n            | '\\u{30A0}'..='\\u{30FF}' // Katakana\n            | '\\u{4E00}'..='\\u{9FFF}' // CJK Unified Ideographs (Kanji)\n        )\n}\n\n/// Validate an addressable endpoint id/name against the charset contract.\n/// Returns `Ok(())` if every scalar is allowed and the length is in `1..=MAX`.\npub fn validate_endpoint_id(id: &str) -> Result<(), IdError> {\n    if id.is_empty() {\n        return Err(IdError::Empty);\n    }\n    let len = id.chars().count();\n    if len > MAX_ENDPOINT_ID_LEN {\n        return Err(IdError::TooLong {\n            len,\n            max: MAX_ENDPOINT_ID_LEN,\n        });\n    }\n    for (pos, ch) in id.chars().enumerate() {\n        if !is_allowed_id_char(ch) {\n            return Err(IdError::BadChar { ch, pos });\n        }\n    }\n    Ok(())\n}\n\n/// Convenience boolean form of [`validate_endpoint_id`].\npub fn is_valid_endpoint_id(id: &str) -> bool {\n    validate_endpoint_id(id).is_ok()\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // [unit->REQ-HAZARD-ID-CHARSET]\n    #[test]\n    fn accepts_plain_and_composite_ids() {\n        for id in [\n            \"alice\",\n            \"doyle\",\n            \"snake_case\",\n            \"kebab-case\",\n            \"doyle-psyche\", // Psyche suffix scheme\n            \"doyle-w331\",   // Worker suffix scheme\n            \"Node1\",\n            \"a\",\n        ] {\n            assert!(is_valid_endpoint_id(id), \"{id} should be valid\");\n        }\n    }\n\n    // [unit->REQ-HAZARD-ID-CHARSET]\n    #[test]\n    fn accepts_japanese_scripts() {\n        for id in [\n            \"こんにちは\",    // Hiragana\n            \"ミックス\",      // Katakana\n            \"日本語\",        // Kanji\n            \"agent-日本-01\", // mixed with ASCII + hyphen\n        ] {\n            assert!(is_valid_endpoint_id(id), \"{id} should be valid\");\n        }\n    }\n\n    // [unit->REQ-HAZARD-ID-CHARSET]\n    #[test]\n    fn reserves_address_delimiters() {\n        // The whole point: `:` and `@` can never appear in a bare id, so\n        // `[subnet:]id[@node]` parsing is unambiguous.\n        assert!(!is_valid_endpoint_id(\"home:ling\"));\n        assert!(!is_valid_endpoint_id(\"ling@hfenduleam\"));\n        assert!(!is_valid_endpoint_id(\"home:ling@node\"));\n    }\n\n    // [unit->REQ-HAZARD-ID-CHARSET]\n    #[test]\n    fn rejects_path_whitespace_and_control() {\n        for bad in [\n            \"a/b\", \"a\\\\b\", \"a.b\", \"a b\", \"a\\tb\", \"a\\nb\", \"..\", \".\", \"a!b\", \"a#b\", \"🦀\",\n        ] {\n            assert!(!is_valid_endpoint_id(bad), \"{bad:?} should be rejected\");\n        }\n    }\n\n    // [unit->REQ-HAZARD-ID-CHARSET]\n    #[test]\n    fn rejects_empty_and_overlong() {\n        assert_eq!(validate_endpoint_id(\"\"), Err(IdError::Empty));\n        let long = \"a\".repeat(MAX_ENDPOINT_ID_LEN + 1);\n        assert!(matches!(\n            validate_endpoint_id(&long),\n            Err(IdError::TooLong { .. })\n        ));\n        assert!(is_valid_endpoint_id(&\"a\".repeat(MAX_ENDPOINT_ID_LEN)));\n    }\n\n    // [unit->REQ-HAZARD-ID-CHARSET]\n    #[test]\n    fn reports_bad_char_position() {\n        assert_eq!(\n            validate_endpoint_id(\"ab:cd\"),\n            Err(IdError::BadChar { ch: ':', pos: 2 })\n        );\n    }\n}\n"}],"details":{"displayContent":{"text":"//! Endpoint-id charset contract — the addressable-name validator.\r\n//!\r\n//! Every addressable endpoint id/name is restricted to a deliberately narrow\r\n//! charset so that:\r\n//!   - `:` and `@` stay **reserved** as the future address delimiters of the\r\n//!     canonical qualified form `[subnet:]id[@node]` (ADR-0006 / REQ-INST-10) —\r\n//!     no bare id can ever contain them, so qualified-address parsing is\r\n//!     unambiguous and no permissive id-data accumulates that would need a\r\n//!     later migration;\r\n//!   - path separators, whitespace, and control characters cannot smuggle into\r\n//!     a perch directory name or a wire target string.\r\n//!\r\n//! Allowed: ASCII alphanumeric, `-`, `_`, and the Japanese script blocks\r\n//! (Hiragana, Katakana, CJK Unified Ideographs). Nothing else. The existing\r\n//! Psyche (`<parent>-psyche`) and Worker (`<parent>-w<N>`) suffix scheme uses\r\n//! only `-` + alphanumerics, so composite ids validate cleanly.\r\n// [impl->REQ-HAZARD-ID-CHARSET]\r\n\r\n/// Maximum endpoint-id length, in Unicode scalar values.\r\npub const MAX_ENDPOINT_ID_LEN: usize = 64;\r\n\r\n/// Why an endpoint id was rejected.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub enum IdError {\r\n    /// The id was the empty string.\r\n    Empty,\r\n    /// The id exceeded [`MAX_ENDPOINT_ID_LEN`] scalar values.\r\n    TooLong { len: usize, max: usize },\r\n    /// A disallowed character was found (with its scalar position).\r\n    BadChar { ch: char, pos: usize },\r\n}\r\n\r\nimpl std::fmt::Display for IdError {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        match self {\r\n            IdError::Empty => write!(f, \"endpoint id must not be empty\"),\r\n            IdError::TooLong { len, max } => {\r\n                write!(f, \"endpoint id too long ({len} chars; max {max})\")\r\n            }\r\n            IdError::BadChar { ch, pos } => write!(\r\n                f,\r\n                \"endpoint id has a disallowed character {ch:?} at position {pos} \\\r\n                 (allowed: A-Z a-z 0-9 - _ and Hiragana/Katakana/Kanji; \\\r\n                 `:` and `@` are reserved address delimiters)\"\r\n            ),\r\n        }\r\n    }\r\n}\r\n\r\nimpl std::error::Error for IdError {}\r\n\r\n/// True if `c` is permitted in an endpoint id.\r\npub fn is_allowed_id_char(c: char) -> bool {\r\n    c.is_ascii_alphanumeric()\r\n        || c == '-'\r\n        || c == '_'\r\n        || matches!(c,\r\n            '\\u{3040}'..='\\u{309F}'   // Hiragana\r\n            | '\\u{30A0}'..='\\u{30FF}' // Katakana\r\n            | '\\u{4E00}'..='\\u{9FFF}' // CJK Unified Ideographs (Kanji)\r\n        )\r\n}\r\n\r\n/// Validate an addressable endpoint id/name against the charset contract.\r\n/// Returns `Ok(())` if every scalar is allowed and the length is in `1..=MAX`.\r\npub fn validate_endpoint_id(id: &str) -> Result<(), IdError> {\r\n    if id.is_empty() {\r\n        return Err(IdError::Empty);\r\n    }\r\n    let len = id.chars().count();\r\n    if len > MAX_ENDPOINT_ID_LEN {\r\n        return Err(IdError::TooLong {\r\n            len,\r\n            max: MAX_ENDPOINT_ID_LEN,\r\n        });\r\n    }\r\n    for (pos, ch) in id.chars().enumerate() {\r\n        if !is_allowed_id_char(ch) {\r\n            return Err(IdError::BadChar { ch, pos });\r\n        }\r\n    }\r\n    Ok(())\r\n}\r\n\r\n/// Convenience boolean form of [`validate_endpoint_id`].\r\npub fn is_valid_endpoint_id(id: &str) -> bool {\r\n    validate_endpoint_id(id).is_ok()\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    // [unit->REQ-HAZARD-ID-CHARSET]\r\n    #[test]\r\n    fn accepts_plain_and_composite_ids() {\r\n        for id in [\r\n            \"alice\",\r\n            \"doyle\",\r\n            \"snake_case\",\r\n            \"kebab-case\",\r\n            \"doyle-psyche\", // Psyche suffix scheme\r\n            \"doyle-w331\",   // Worker suffix scheme\r\n            \"Node1\",\r\n            \"a\",\r\n        ] {\r\n            assert!(is_valid_endpoint_id(id), \"{id} should be valid\");\r\n        }\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-ID-CHARSET]\r\n    #[test]\r\n    fn accepts_japanese_scripts() {\r\n        for id in [\r\n            \"こんにちは\",    // Hiragana\r\n            \"ミックス\",      // Katakana\r\n            \"日本語\",        // Kanji\r\n            \"agent-日本-01\", // mixed with ASCII + hyphen\r\n        ] {\r\n            assert!(is_valid_endpoint_id(id), \"{id} should be valid\");\r\n        }\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-ID-CHARSET]\r\n    #[test]\r\n    fn reserves_address_delimiters() {\r\n        // The whole point: `:` and `@` can never appear in a bare id, so\r\n        // `[subnet:]id[@node]` parsing is unambiguous.\r\n        assert!(!is_valid_endpoint_id(\"home:ling\"));\r\n        assert!(!is_valid_endpoint_id(\"ling@hfenduleam\"));\r\n        assert!(!is_valid_endpoint_id(\"home:ling@node\"));\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-ID-CHARSET]\r\n    #[test]\r\n    fn rejects_path_whitespace_and_control() {\r\n        for bad in [\r\n            \"a/b\", \"a\\\\b\", \"a.b\", \"a b\", \"a\\tb\", \"a\\nb\", \"..\", \".\", \"a!b\", \"a#b\", \"🦀\",\r\n        ] {\r\n            assert!(!is_valid_endpoint_id(bad), \"{bad:?} should be rejected\");\r\n        }\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-ID-CHARSET]\r\n    #[test]\r\n    fn rejects_empty_and_overlong() {\r\n        assert_eq!(validate_endpoint_id(\"\"), Err(IdError::Empty));\r\n        let long = \"a\".repeat(MAX_ENDPOINT_ID_LEN + 1);\r\n        assert!(matches!(\r\n            validate_endpoint_id(&long),\r\n            Err(IdError::TooLong { .. })\r\n        ));\r\n        assert!(is_valid_endpoint_id(&\"a\".repeat(MAX_ENDPOINT_ID_LEN)));\r\n    }\r\n\r\n    // [unit->REQ-HAZARD-ID-CHARSET]\r\n    #[test]\r\n    fn reports_bad_char_position() {\r\n        assert_eq!(\r\n            validate_endpoint_id(\"ab:cd\"),\r\n            Err(IdError::BadChar { ch: ':', pos: 2 })\r\n        );\r\n    }\r\n}\r\n","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-proto\\src\\id.rs"}}},"isError":false,"timestamp":1784197494178}}
{"type":"message","id":"62d90ef8","parentId":"8e1c7937","timestamp":"2026-07-16T10:24:54.180Z","message":{"role":"toolResult","toolCallId":"call_Cpwv3TEFNFJFSmZuZwnqEZIK|fc_017bff223290b7ba016a58b1760e84819a80eac76c9c11ec6c","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt-proto/src/\n### id.rs\n 12|//!\n*13|//! Allowed: ASCII alphanumeric, `-`, `_`, and the Japanese script blocks\n 14|//! (Hiragana, Katakana, CJK Unified Ideographs). Nothing else. The existing\n 15|//! Psyche (`<parent>-psyche`) and Worker (`<parent>-w<N>`) suffix scheme uses\n 16|//! only `-` + alphanumerics, so composite ids validate cleanly.\n...\n 53|pub fn is_allowed_id_char(c: char) -> bool {\n*54|    c.is_ascii_alphanumeric()\n*55|        || c == '-'\n 56|        || c == '_'\n 57|        || matches!(c,\n 58|            '\\u{3040}'..='\\u{309F}'   // Hiragana\n...\n 65|/// Returns `Ok(())` if every scalar is allowed and the length is in `1..=MAX`.\n*66|pub fn validate_endpoint_id(id: &str) -> Result<(), IdError> {\n 67|    if id.is_empty() {\n 68|        return Err(IdError::Empty);\n 69|    }\n...\n 84|\n*85|/// Convenience boolean form of [`validate_endpoint_id`].\n 86|pub fn is_valid_endpoint_id(id: &str) -> bool {\n*87|    validate_endpoint_id(id).is_ok()\n 88|}\n 89|\n 90|#[cfg(test)]\n...\n 117|            \"日本語\",        // Kanji\n*118|            \"agent-日本-01\", // mixed with ASCII + hyphen\n 119|        ] {\n 120|            assert!(is_valid_endpoint_id(id), \"{id} should be valid\");\n 121|        }\n...\n 146|    fn rejects_empty_and_overlong() {\n*147|        assert_eq!(validate_endpoint_id(\"\"), Err(IdError::Empty));\n 148|        let long = \"a\".repeat(MAX_ENDPOINT_ID_LEN + 1);\n 149|        assert!(matches!(\n*150|            validate_endpoint_id(&long),\n 151|            Err(IdError::TooLong { .. })\n 152|        ));\n 153|        assert!(is_valid_endpoint_id(&\"a\".repeat(MAX_ENDPOINT_ID_LEN)));\n...\n 159|        assert_eq!(\n*160|            validate_endpoint_id(\"ab:cd\"),\n 161|            Err(IdError::BadChar { ch: ':', pos: 2 })\n 162|        );\n 163|    }\n\n## spt/src/\n### cli.rs\n 1907|    // [impl->REQ-HAZARD-ID-CHARSET]\n*1908|    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {\n*1909|        eprintln!(\"INVALID_ID:{id}: {e}\");\n 1910|        return 2;\n 1911|    }\n 1912|    // Resolve the harness adapter option (`<adapter>[:profile]`) through the\n...\n 2280|            let ans = line.trim();\n*2281|            if ans.eq_ignore_ascii_case(\"n\") || ans.eq_ignore_ascii_case(\"no\") {\n 2282|                eprintln!(\n 2283|                    \"MULTI_SUBNET_HOME:{id}: cancelled — re-run with --subnet <name> ({})\",\n 2284|                    subnets.join(\", \")\n...\n 2530|\n*2531|    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {\n 2532|        eprintln!(\"ERROR: invalid new id: {e}\");\n 2533|        return 2;\n 2534|    }\n...\n 3028|        // O(P×B+C) per-perch git fanout is retired — readers never run git;\n*3029|        // absent/stale renders None → the same '-' semantics).\n 3030|        // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\n 3031|        let index = spt_store::projindex::read_index();\n 3032|        let local = all_local\n...\n 8461|fn prune_candidates(roster: &spt_store::roster::RosterStore, query: &str) -> Vec<String> {\n*8462|    let q = query.to_ascii_lowercase();\n 8463|    let mut out: Vec<String> = Vec::new();\n 8464|    for m in &roster.members {\n 8465|        let hit = m.pubkey_hex == q\n 8466|            || m.pubkey_hex.starts_with(&q)\n*8467|            || (!m.label.is_empty() && m.label.eq_ignore_ascii_case(query));\n 8468|        if hit && !out.contains(&m.pubkey_hex) {\n 8469|            out.push(m.pubkey_hex.clone());\n 8470|        }\n...\n 9433|    fn valid_node_hex(node: &str) -> bool {\n*9434|        node.len() == 64 && node.chars().all(|c| c.is_ascii_hexdigit())\n 9435|    }\n 9436|    fn save(store: &AccessStore) -> i32 {\n 9437|        match store.save() {\n...\n 11361|            if let Some(a) = &alias {\n*11362|                if let Err(e) = spt_proto::id::validate_endpoint_id(a) {\n 11363|                    eprintln!(\"INVALID_ALIAS:{a}: {e}\");\n 11364|                    return 2;\n 11365|                }\n...\n 11661|            // [impl->REQ-HAZARD-ID-CHARSET]\n*11662|            if let Err(e) = spt_proto::id::validate_endpoint_id(&alias) {\n 11663|                eprintln!(\"INVALID_ALIAS:{alias}: {e}\");\n 11664|                return 2;\n 11665|            }\n...\n 12062|    use spt_daemon::grants::EscalationAnswer;\n*12063|    match input.trim().to_ascii_lowercase().as_str() {\n 12064|        \"o\" | \"once\" => EscalationAnswer::AllowOnce,\n 12065|        \"a\" | \"always\" if persist_allowed => EscalationAnswer::AllowAlways,\n 12066|        _ => EscalationAnswer::Deny,\n...\n 12377|fn cmd_rename(old_id: &str, new_id: &str) -> i32 {\n*12378|    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {\n 12379|        eprintln!(\"ERROR: invalid new id: {e}\");\n 12380|        return 2;\n 12381|    }\n...\n 13567|        assert!(lines[2].contains(\" owl/ \"), \"unique project renders bare id + slash: {out}\");\n*13568|        assert!(lines[3].contains(\" -  \"), \"unknown project renders '-': {out}\");\n 13569|        // The project column sits between id and status (id / <project>/ / type / status).\n 13570|        assert!(\n 13571|            lines[2].find(\" owl/\").unwrap() < lines[2].find(\"ONLINE\").unwrap(),\n...\n 13577|    // from the #4-gossiped `recent_projects` (newest) — IDs only, NO dir on the wire; a\n*13578|    // pre-field remote row (empty recent_projects) has no project → the column renders '-'.\n 13579|    #[test]\n 13580|    fn remote_cell_project_ref_from_gossiped_recent_projects() {\n 13581|        use spt_net::net::registry::Status;\n...\n 13593|            instance_cell_from_resource(&row).project_ref.is_none(),\n*13594|            \"pre-field remote row → no project (renders '-')\"\n 13595|        );\n 13596|    }\n 13597|\n...\n 18687|        // A built adapter living in the durable _github home — exactly where a\n*18688|        // `--release` / `--github` add lands it (safe = spec with '/'→'-'). A\n 18689|        // delegated avenue records it POINTER-mode, the perri footgun's shape.\n 18690|        let spec = \"acme/cc-adapter\";\n 18691|        let safe = \"acme-cc-adapter\";"}],"details":{"scopePath":"crates/spt-proto/src/id.rs, crates/spt/src/cli.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":26,"fileCount":2,"files":["crates/spt-proto/src/id.rs","crates/spt/src/cli.rs"],"fileMatches":[{"path":"crates/spt-proto/src/id.rs","count":10},{"path":"crates/spt/src/cli.rs","count":16}],"truncated":false,"displayContent":"# crates/\n\n## spt-proto/src/\n### id.rs\n  12│//!\n *13│//! Allowed: ASCII alphanumeric, `-`, `_`, and the Japanese script blocks\n  14│//! (Hiragana, Katakana, CJK Unified Ideographs). Nothing else. The existing\n  15│//! Psyche (`<parent>-psyche`) and Worker (`<parent>-w<N>`) suffix scheme uses\n  16│//! only `-` + alphanumerics, so composite ids validate cleanly.\n    │...\n  53│pub fn is_allowed_id_char(c: char) -> bool {\n *54│    c.is_ascii_alphanumeric()\n *55│        || c == '-'\n  56│        || c == '_'\n  57│        || matches!(c,\n  58│            '\\u{3040}'..='\\u{309F}'   // Hiragana\n    │...\n  65│/// Returns `Ok(())` if every scalar is allowed and the length is in `1..=MAX`.\n *66│pub fn validate_endpoint_id(id: &str) -> Result<(), IdError> {\n  67│    if id.is_empty() {\n  68│        return Err(IdError::Empty);\n  69│    }\n    │...\n  84│\n *85│/// Convenience boolean form of [`validate_endpoint_id`].\n  86│pub fn is_valid_endpoint_id(id: &str) -> bool {\n *87│    validate_endpoint_id(id).is_ok()\n  88│}\n  89│\n  90│#[cfg(test)]\n    │...\n 117│            \"日本語\",        // Kanji\n*118│            \"agent-日本-01\", // mixed with ASCII + hyphen\n 119│        ] {\n 120│            assert!(is_valid_endpoint_id(id), \"{id} should be valid\");\n 121│        }\n    │...\n 146│    fn rejects_empty_and_overlong() {\n*147│        assert_eq!(validate_endpoint_id(\"\"), Err(IdError::Empty));\n 148│        let long = \"a\".repeat(MAX_ENDPOINT_ID_LEN + 1);\n 149│        assert!(matches!(\n*150│            validate_endpoint_id(&long),\n 151│            Err(IdError::TooLong { .. })\n 152│        ));\n 153│        assert!(is_valid_endpoint_id(&\"a\".repeat(MAX_ENDPOINT_ID_LEN)));\n    │...\n 159│        assert_eq!(\n*160│            validate_endpoint_id(\"ab:cd\"),\n 161│            Err(IdError::BadChar { ch: ':', pos: 2 })\n 162│        );\n 163│    }\n\n## spt/src/\n### cli.rs\n  1907│    // [impl->REQ-HAZARD-ID-CHARSET]\n *1908│    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {\n *1909│        eprintln!(\"INVALID_ID:{id}: {e}\");\n  1910│        return 2;\n  1911│    }\n  1912│    // Resolve the harness adapter option (`<adapter>[:profile]`) through the\n      │...\n  2280│            let ans = line.trim();\n *2281│            if ans.eq_ignore_ascii_case(\"n\") || ans.eq_ignore_ascii_case(\"no\") {\n  2282│                eprintln!(\n  2283│                    \"MULTI_SUBNET_HOME:{id}: cancelled — re-run with --subnet <name> ({})\",\n  2284│                    subnets.join(\", \")\n      │...\n  2530│\n *2531│    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {\n  2532│        eprintln!(\"ERROR: invalid new id: {e}\");\n  2533│        return 2;\n  2534│    }\n      │...\n  3028│        // O(P×B+C) per-perch git fanout is retired — readers never run git;\n *3029│        // absent/stale renders None → the same '-' semantics).\n  3030│        // [impl->REQ-PROJECT-INDEX-READER-CUTOVER]\n  3031│        let index = spt_store::projindex::read_index();\n  3032│        let local = all_local\n      │...\n  8461│fn prune_candidates(roster: &spt_store::roster::RosterStore, query: &str) -> Vec<String> {\n *8462│    let q = query.to_ascii_lowercase();\n  8463│    let mut out: Vec<String> = Vec::new();\n  8464│    for m in &roster.members {\n  8465│        let hit = m.pubkey_hex == q\n  8466│            || m.pubkey_hex.starts_with(&q)\n *8467│            || (!m.label.is_empty() && m.label.eq_ignore_ascii_case(query));\n  8468│        if hit && !out.contains(&m.pubkey_hex) {\n  8469│            out.push(m.pubkey_hex.clone());\n  8470│        }\n      │...\n  9433│    fn valid_node_hex(node: &str) -> bool {\n *9434│        node.len() == 64 && node.chars().all(|c| c.is_ascii_hexdigit())\n  9435│    }\n  9436│    fn save(store: &AccessStore) -> i32 {\n  9437│        match store.save() {\n      │...\n 11361│            if let Some(a) = &alias {\n*11362│                if let Err(e) = spt_proto::id::validate_endpoint_id(a) {\n 11363│                    eprintln!(\"INVALID_ALIAS:{a}: {e}\");\n 11364│                    return 2;\n 11365│                }\n      │...\n 11661│            // [impl->REQ-HAZARD-ID-CHARSET]\n*11662│            if let Err(e) = spt_proto::id::validate_endpoint_id(&alias) {\n 11663│                eprintln!(\"INVALID_ALIAS:{alias}: {e}\");\n 11664│                return 2;\n 11665│            }\n      │...\n 12062│    use spt_daemon::grants::EscalationAnswer;\n*12063│    match input.trim().to_ascii_lowercase().as_str() {\n 12064│        \"o\" | \"once\" => EscalationAnswer::AllowOnce,\n 12065│        \"a\" | \"always\" if persist_allowed => EscalationAnswer::AllowAlways,\n 12066│        _ => EscalationAnswer::Deny,\n      │...\n 12377│fn cmd_rename(old_id: &str, new_id: &str) -> i32 {\n*12378│    if let Err(e) = spt_proto::id::validate_endpoint_id(new_id) {\n 12379│        eprintln!(\"ERROR: invalid new id: {e}\");\n 12380│        return 2;\n 12381│    }\n      │...\n 13567│        assert!(lines[2].contains(\" owl/ \"), \"unique project renders bare id + slash: {out}\");\n*13568│        assert!(lines[3].contains(\" -  \"), \"unknown project renders '-': {out}\");\n 13569│        // The project column sits between id and status (id / <project>/ / type / status).\n 13570│        assert!(\n 13571│            lines[2].find(\" owl/\").unwrap() < lines[2].find(\"ONLINE\").unwrap(),\n      │...\n 13577│    // from the #4-gossiped `recent_projects` (newest) — IDs only, NO dir on the wire; a\n*13578│    // pre-field remote row (empty recent_projects) has no project → the column renders '-'.\n 13579│    #[test]\n 13580│    fn remote_cell_project_ref_from_gossiped_recent_projects() {\n 13581│        use spt_net::net::registry::Status;\n      │...\n 13593│            instance_cell_from_resource(&row).project_ref.is_none(),\n*13594│            \"pre-field remote row → no project (renders '-')\"\n 13595│        );\n 13596│    }\n 13597│\n      │...\n 18687│        // A built adapter living in the durable _github home — exactly where a\n*18688│        // `--release` / `--github` add lands it (safe = spec with '/'→'-'). A\n 18689│        // delegated avenue records it POINTER-mode, the perri footgun's shape.\n 18690│        let spec = \"acme/cc-adapter\";\n 18691│        let safe = \"acme-cc-adapter\";"},"isError":false,"timestamp":1784197494180}}
{"type":"message","id":"3de7f59b","parentId":"62d90ef8","timestamp":"2026-07-16T10:25:04.262Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Investigating hyphen parsing edge cases**\n\n**Deferring elevation docs retrieval**"},{"type":"toolCall","id":"call_mjGyPxrBF8rdhOcK5x0ma6ZJ|fc_017bff223290b7ba016a58b1803d24819aa209236a4cba9e55","name":"read","arguments":{"i":"Reading elevated endpoint deferral","path":"docs/DEFERRED.md","selector":"24-29"}},{"type":"toolCall","id":"call_Nbz49jT1uncFQj5raBoVW8nj|fc_017bff223290b7ba016a58b1803d34819ab22d49ed8e2a2a2d","name":"read","arguments":{"i":"Reading daemon de-elevation enforcement","path":"crates/spt-daemon/src/deelevate.rs","selector":"1-75"}},{"type":"toolCall","id":"call_qCL69KhKl3UDAaZQuGpbqeU3|fc_017bff223290b7ba016a58b1803d40819aa0f93a25fa1b4b6e","name":"grep","arguments":{"i":"Finding daemon spawn de-elevation","pattern":"fn spawn_detached|spawn_deelevated|SpawnPlan|plan_spawn|daemon_target_user","path":"crates/spt-daemon/src/daemon.rs; crates/spt-daemon/src/deelevate.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_blUNslIsUldIVj5Kpp8kPSTk|fc_017bff223290b7ba016a58b1803d44819ab7c0f9297f026c01","name":"read","arguments":{"i":"Reading harness brokered ownership","path":"crates/spt-daemon/src/harnesshost.rs","selector":"1-180"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":7330,"output":394,"cacheRead":195072,"cacheWrite":0,"totalTokens":202796,"reasoningTokens":180,"cost":{"input":0.03665,"output":0.01182,"cacheRead":0.097536,"cacheWrite":0,"total":0.146006}},"stopReason":"toolUse","timestamp":1784197494188,"responseId":"resp_017bff223290b7ba016a58b1768fcc819a897149217f240230","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b1788ef4819aa069217302838377","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLGA9yZKF9GQTy4-XlWuOZtySZJkhOJdIiqEZOkqJms4ntSBb7HbiQ9wd3ea2vSBsg-l6BZzA_oaJ3gBuB_Ji4PqJB1ECnZtY1ZX_qxt11TnCTdFupj8xB1sv7yGc9GVB5wsvmSEh-Tzo0ZgGMA5YgQFMM1thtRvgO4ww5zfokiw1MV53QHDbl2NPVCpGFoGnWTHsnBdzWK-4Wh71QWm927AyIg1_zlXtmleIwgm_84PZqtYavxjjJbwoxABnNXt9l4i4zKg7jGPpl7GAgq-DDnlr_xoBDUIkRf8WghpqVa_sF_2V6ZEdIBiNb1UQn5ESxOg3k7ol90cVWbBmFsNmhL4AsPg5uN5cd6eIYAQ-JXgxI88r8PVWwL79yXNVYjoxa0emTVWhgkEuAOXU8Z0A0rOZr4xNo7z9mtvGsu9GnkFT26QK4vtITHCZr3Je35JUJfsv1cWItVPmOgJ7gbgZ48FAwezQPMHb3hwuLOAoZey4ys_7KikC0aolmyXFdgpd_Pt67RIIVY56TriD657GrKiIAUB-WTNctK0IiI1ypLWOJtfoV-vWgkkg-jBNHVOyHUuodWJ45B4pDzFJq5upsWIGet0tXkuUpHFb_jRpfSSON0MviWJSVsAcbfxIviNIh5nnBjJ0WpLF1fgmee80jD-HD71mzzo5exxYPC4HxgfYQy9WZ_EzrnTwXHmjPBL6UIG9ji6oHkQBlATq7kpwXPZqT5mMEfWE298tuCEQ0wvavsvzVSzQs47p2zcY8unX6njN-Q7Kx9yvQqIA7x3t8aC8MLMtyKGV-u_3CSm8tj8lGcbljovRhHnR09SwQTgdQY7tLHTaLw5e4msJz_VxnZ9SqdL639nAtHVHsKqNC6Zdt30PpZgw9j9zj-XK1XNPSvH-q7jjMuSeDW3SdokUyASMirA9IeE9z7memz9hNbnQdU9y5_e5YHvzr4P_rUsCMG2wUE7hRhgyUaM6EjSwCLzaH6hN2POPovePhpd9NOQlNKi0DMAsKWKmogeQw6xReeFytUKakzJpHAvAFjEoQ7xFwEgHmDyImx5a3kFZb_W-GX-tXwVd9wKXW3k-wQ7F0W5noI3XmYu4vZxpkRvfbrcFxcJRoH8Oef5ME7HRyVezURYyW6xpTgMDdRr0csZ3JiQEFIRicxtMHWNpK0nvbUJhx5gyS0lMAZDKN8bVgVDRQ-DAU18a-k8ICR11O4YPwAIbYScB54bPa_edH9YXKnshyIHmnB7-waFAyOlnxg4f7MPXY8cc9lI-aRvKWq26WFrhxZmPwwlPpUlbuWSz442m_QmzCCe2XBgxy1JOZK5rIXMvAqsymuQUbskYOWCpE1Q5yB8Edq7--V943a_NJRfVji4Pvqa2pfQsy_GZjL0M9MzZ9JlWx-80q3IslisUVXfzHUqD_etDsoFV8zNMVZdCnLEDYwFFgMwZscLllHYMHdKl2yVILmvQ-Fn14XDMJQ65zP0PzQnA01j9J-qFvId9k2Bha0ocfkTUKNuH9VUBSw1j3xMiMiv1wXTwmqApC3_AhMAeyMCFa3hckTtN4lNV-unAxg-GyjfeLJafKDKe1TFH1fkyVT_8xkA1qW4QTVR_x_jb42ElldWNYQTMpMaFCMC3WjVYFAERjjMRWHWxhSGtVNNJ_UpgSJRrfLz5cS-9ONm1k4k9YsBfGvbLDvXTx17M-Z2Mc1f10ZX4EBEC9nSBM7nwIR1lAVmkx2GJRgUlT3EYlc9axNKkwHUGUStjy8mPYJRM3rODMHKKNPdFMBqnP35ovTCPsTn0fanDTMN79BKp9ayZfD13KZI2lQxuxdgkfvLKWRIKBIpc869H578tAsvOVmJdWeCxh84ySsE7oUTXenJYsJXsAJmBPSt0mTYFLhO7TbVGeQ5EbGHmejp9uVMDFjtZJu3qlVUeFrpFEckInKD0olMNUoHClBN78Y8x5ntiDn0vWTe3lDUNSiX6T9JFS7ude9y0OE7nchOobDtZf9IVLsR5pUal7FGEA==","summary":[{"type":"summary_text","text":"**Investigating hyphen parsing edge cases**"},{"type":"summary_text","text":"**Deferring elevation docs retrieval**"}]},{"id":"fc_017bff223290b7ba016a58b1803d24819aa209236a4cba9e55","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading elevated endpoint deferral\",\"path\":\"docs/DEFERRED.md\",\"selector\":\"24-29\"}","call_id":"call_mjGyPxrBF8rdhOcK5x0ma6ZJ","name":"read"},{"id":"fc_017bff223290b7ba016a58b1803d34819ab22d49ed8e2a2a2d","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading daemon de-elevation enforcement\",\"path\":\"crates/spt-daemon/src/deelevate.rs\",\"selector\":\"1-75\"}","call_id":"call_Nbz49jT1uncFQj5raBoVW8nj","name":"read"},{"id":"fc_017bff223290b7ba016a58b1803d40819aa0f93a25fa1b4b6e","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding daemon spawn de-elevation\",\"pattern\":\"fn spawn_detached|spawn_deelevated|SpawnPlan|plan_spawn|daemon_target_user\",\"path\":\"crates/spt-daemon/src/daemon.rs; crates/spt-daemon/src/deelevate.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_qCL69KhKl3UDAaZQuGpbqeU3","name":"grep"},{"id":"fc_017bff223290b7ba016a58b1803d44819ab7c0f9297f026c01","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading harness brokered ownership\",\"path\":\"crates/spt-daemon/src/harnesshost.rs\",\"selector\":\"1-180\"}","call_id":"call_blUNslIsUldIVj5Kpp8kPSTk","name":"read"}]},"duration":10072.868300000206,"ttft":2346.4851000001654,"contextSnapshot":{"promptTokens":202402,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_mjGyPxrBF8rdhOcK5x0ma6ZJ|fc_017bff223290b7ba016a58b1803d24819aa209236a4cba9e55","toolName":"read","startedAt":"2026-07-16T10:25:04.263Z","args":{"path":"docs/DEFERRED.md"},"intent":"Reading elevated endpoint deferral"},"id":"8a238871","parentId":"3de7f59b","timestamp":"2026-07-16T10:25:04.263Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Nbz49jT1uncFQj5raBoVW8nj|fc_017bff223290b7ba016a58b1803d34819ab22d49ed8e2a2a2d","toolName":"read","startedAt":"2026-07-16T10:25:04.263Z","args":{"path":"crates/spt-daemon/src/deelevate.rs"},"intent":"Reading daemon de-elevation enforcement"},"id":"d30cb421","parentId":"8a238871","timestamp":"2026-07-16T10:25:04.263Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_qCL69KhKl3UDAaZQuGpbqeU3|fc_017bff223290b7ba016a58b1803d40819aa0f93a25fa1b4b6e","toolName":"grep","startedAt":"2026-07-16T10:25:04.263Z","args":{"path":"crates/spt-daemon/src/daemon.rs; crates/spt-daemon/src/deelevate.rs"},"intent":"Finding daemon spawn de-elevation"},"id":"3487ae5e","parentId":"d30cb421","timestamp":"2026-07-16T10:25:04.263Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_blUNslIsUldIVj5Kpp8kPSTk|fc_017bff223290b7ba016a58b1803d44819ab7c0f9297f026c01","toolName":"read","startedAt":"2026-07-16T10:25:04.263Z","args":{"path":"crates/spt-daemon/src/harnesshost.rs"},"intent":"Reading harness brokered ownership"},"id":"11938907","parentId":"3487ae5e","timestamp":"2026-07-16T10:25:04.263Z"}
{"type":"message","id":"baa4d2e0","parentId":"11938907","timestamp":"2026-07-16T10:25:04.267Z","message":{"role":"toolResult","toolCallId":"call_qCL69KhKl3UDAaZQuGpbqeU3|fc_017bff223290b7ba016a58b1803d40819aa0f93a25fa1b4b6e","toolName":"grep","content":[{"type":"text","text":"# crates/spt-daemon/src/\n## daemon.rs\n 73|        #[cfg(unix)]\n*74|        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n 75|            match invoker.drop_in_process() {\n 76|                Ok(()) => eprintln!(\"DEELEVATED: daemon dropped to uid {}\", invoker.uid),\n 77|                // A drop target existed but the drop failed: serving as\n...\n 90|            let exe = std::env::current_exe()?;\n*91|            match crate::deelevate::spawn_deelevated(\n 92|                &exe.to_string_lossy(),\n 93|                // --detached BELT (REQ-HAZARD-DETACHED-DAEMON-STDIO): the respawned\n 94|                // unelevated daemon then runs detach_console + the null-handles guard,\n...\n 631|/// an elevated spawner (Windows UAC, `sudo`) de-elevates the child here —\n*632|/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n 633|/// no handle inheritance by construction, so 5.6 holds on that path too),\n 634|/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n 635|/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n...\n 638|// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n*639|fn spawn_detached() -> io::Result<()> {\n 640|    let exe = std::env::current_exe()?;\n 641|    #[cfg(windows)]\n 642|    {\n...\n 648|        // WRAPPER, but the inner broker (a console-subsystem exe) still ALLOCATES its\n*649|        // own conhost = a window. The flag rides every rung (spawn_deelevated +\n 650|        // launch_daemon_job_neutral); a human-typed `spt daemon run` omits it and\n 651|        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n 652|        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n...\n 657|        ];\n*658|        match crate::deelevate::spawn_deelevated(&exe, &args) {\n 659|            Ok(Some(_pid)) => return Ok(()), // de-elevated child running\n 660|            Ok(None) => {\n 661|                if crate::deelevate::is_elevated() {\n...\n 694|            .stderr(Stdio::null());\n*695|        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n 696|            // Drop the child to the elected default user (else the sudo\n 697|            // invoker — M8 decision 8): std clears supplementary groups and\n 698|            // orders setgid-before-setuid when uid is set.\n## deelevate.rs\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...\n 38|#[cfg(unix)]\n*39|pub use unix::{daemon_target_user, sudo_invoker, SudoInvoker};\n 40|\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...\n 47|#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n*48|pub enum SpawnPlan {\n 49|    /// Not elevated (or no de-elevation target exists): spawn as-is.\n 50|    Inherit,\n 51|    /// Elevated with an unelevated identity available: spawn de-elevated.\n...\n 59|// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n*60|pub fn plan_spawn(elevated: bool, has_deelevation_target: bool) -> SpawnPlan {\n 61|    if elevated && has_deelevation_target {\n*62|        SpawnPlan::DeElevate\n 63|    } else {\n*64|        SpawnPlan::Inherit\n 65|    }\n 66|}\n 67|\n...\n 276|    // [impl->REQ-INSTALL-6]\n*277|    pub fn daemon_target_user() -> Option<SudoInvoker> {\n 278|        if unsafe { libc::geteuid() } != 0 {\n 279|            return None;\n 280|        }\n...\n 581|    // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n*582|    pub fn spawn_deelevated(program: &str, args: &[String]) -> io::Result<Option<u32>> {\n 583|        if !is_elevated() {\n 584|            return Ok(None);\n 585|        }\n...\n 606|    /// those cases `run` serves elevated in the foreground consistently rather\n*607|    /// than vanishing. Mirrors [`spawn_deelevated`]'s `Ok(Some)` condition\n 608|    /// WITHOUT spawning, so `daemon run` can refuse only the vanishing case\n 609|    /// (REQ-DAEMON-7).\n 610|    // [impl->REQ-DAEMON-7]\n...\n 817|    #[test]\n*818|    fn plan_spawn_deelevates_only_with_a_target() {\n*819|        assert_eq!(plan_spawn(false, false), SpawnPlan::Inherit);\n*820|        assert_eq!(plan_spawn(false, true), SpawnPlan::Inherit);\n*821|        assert_eq!(plan_spawn(true, false), SpawnPlan::Inherit);\n*822|        assert_eq!(plan_spawn(true, true), SpawnPlan::DeElevate);\n 823|    }\n 824|\n 825|    /// Build a double-null-terminated env block from `NAME=VALUE` strings."}],"details":{"scopePath":"crates/spt-daemon/src/daemon.rs, crates/spt-daemon/src/deelevate.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":22,"fileCount":2,"files":["crates/spt-daemon/src/daemon.rs","crates/spt-daemon/src/deelevate.rs"],"fileMatches":[{"path":"crates/spt-daemon/src/daemon.rs","count":7},{"path":"crates/spt-daemon/src/deelevate.rs","count":15}],"truncated":false,"displayContent":"# crates/spt-daemon/src/\n## daemon.rs\n  73│        #[cfg(unix)]\n *74│        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n  75│            match invoker.drop_in_process() {\n  76│                Ok(()) => eprintln!(\"DEELEVATED: daemon dropped to uid {}\", invoker.uid),\n  77│                // A drop target existed but the drop failed: serving as\n    │...\n  90│            let exe = std::env::current_exe()?;\n *91│            match crate::deelevate::spawn_deelevated(\n  92│                &exe.to_string_lossy(),\n  93│                // --detached BELT (REQ-HAZARD-DETACHED-DAEMON-STDIO): the respawned\n  94│                // unelevated daemon then runs detach_console + the null-handles guard,\n    │...\n 631│/// an elevated spawner (Windows UAC, `sudo`) de-elevates the child here —\n*632│/// Windows via the UAC linked token ([`crate::deelevate::spawn_deelevated`];\n 633│/// no handle inheritance by construction, so 5.6 holds on that path too),\n 634│/// unix by dropping the child to `SUDO_UID`/`SUDO_GID` with the invoker's\n 635│/// `HOME`. When no unelevated identity exists (UAC off, genuine root), the\n    │...\n 638│// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n*639│fn spawn_detached() -> io::Result<()> {\n 640│    let exe = std::env::current_exe()?;\n 641│    #[cfg(windows)]\n 642│    {\n    │...\n 648│        // WRAPPER, but the inner broker (a console-subsystem exe) still ALLOCATES its\n*649│        // own conhost = a window. The flag rides every rung (spawn_deelevated +\n 650│        // launch_daemon_job_neutral); a human-typed `spt daemon run` omits it and\n 651│        // keeps its foreground console (debug). No-op on the breakaway/injob rungs\n 652│        // (already DETACHED_PROCESS → no console → FreeConsole is harmless).\n    │...\n 657│        ];\n*658│        match crate::deelevate::spawn_deelevated(&exe, &args) {\n 659│            Ok(Some(_pid)) => return Ok(()), // de-elevated child running\n 660│            Ok(None) => {\n 661│                if crate::deelevate::is_elevated() {\n    │...\n 694│            .stderr(Stdio::null());\n*695│        if let Some(invoker) = crate::deelevate::daemon_target_user() {\n 696│            // Drop the child to the elected default user (else the sudo\n 697│            // invoker — M8 decision 8): std clears supplementary groups and\n 698│            // orders setgid-before-setuid when uid is set.\n## deelevate.rs\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    │...\n  38│#[cfg(unix)]\n *39│pub use unix::{daemon_target_user, sudo_invoker, SudoInvoker};\n  40│\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    │...\n  47│#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n *48│pub enum SpawnPlan {\n  49│    /// Not elevated (or no de-elevation target exists): spawn as-is.\n  50│    Inherit,\n  51│    /// Elevated with an unelevated identity available: spawn de-elevated.\n    │...\n  59│// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n *60│pub fn plan_spawn(elevated: bool, has_deelevation_target: bool) -> SpawnPlan {\n  61│    if elevated && has_deelevation_target {\n *62│        SpawnPlan::DeElevate\n  63│    } else {\n *64│        SpawnPlan::Inherit\n  65│    }\n  66│}\n  67│\n    │...\n 276│    // [impl->REQ-INSTALL-6]\n*277│    pub fn daemon_target_user() -> Option<SudoInvoker> {\n 278│        if unsafe { libc::geteuid() } != 0 {\n 279│            return None;\n 280│        }\n    │...\n 581│    // [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n*582│    pub fn spawn_deelevated(program: &str, args: &[String]) -> io::Result<Option<u32>> {\n 583│        if !is_elevated() {\n 584│            return Ok(None);\n 585│        }\n    │...\n 606│    /// those cases `run` serves elevated in the foreground consistently rather\n*607│    /// than vanishing. Mirrors [`spawn_deelevated`]'s `Ok(Some)` condition\n 608│    /// WITHOUT spawning, so `daemon run` can refuse only the vanishing case\n 609│    /// (REQ-DAEMON-7).\n 610│    // [impl->REQ-DAEMON-7]\n    │...\n 817│    #[test]\n*818│    fn plan_spawn_deelevates_only_with_a_target() {\n*819│        assert_eq!(plan_spawn(false, false), SpawnPlan::Inherit);\n*820│        assert_eq!(plan_spawn(false, true), SpawnPlan::Inherit);\n*821│        assert_eq!(plan_spawn(true, false), SpawnPlan::Inherit);\n*822│        assert_eq!(plan_spawn(true, true), SpawnPlan::DeElevate);\n 823│    }\n 824│\n 825│    /// Build a double-null-terminated env block from `NAME=VALUE` strings."},"isError":false,"timestamp":1784197504267}}
{"type":"message","id":"f9bd6a6f","parentId":"baa4d2e0","timestamp":"2026-07-16T10:25:04.289Z","message":{"role":"toolResult","toolCallId":"call_blUNslIsUldIVj5Kpp8kPSTk|fc_017bff223290b7ba016a58b1803d44819ab7c0f9297f026c01","toolName":"read","content":[{"type":"text","text":"//! Harness-instance hosting (M12-W1 — spt-hosted harness bringup, REQ-HOST-RUN-1):\n//! the spt-HOSTED counterpart to harness-hosted launch.\n//!\n//! **Who spawns the harness.** Today a harness is *harness-hosted*: an external\n//! launcher runs the adapter's `[session.self]` command and the harness binds its\n//! own perch on startup (`api listen`/`api bind` → `establish_perch`). spt-hosting\n//! changes *only who owns the PTY*: the daemon spawns `[session.self]` into a\n//! **broker-held PTY** ([`launch_harness_brokered_in`]). The harness still\n//! self-registers its perch on bind — there is no daemon pre-register step.\n//!\n//! **Why broker-held.** The broker owns the PTY, so the harness survives a\n//! `spt rc` detach — PTY ownership stays with the broker, no termination on\n//! detach (the KNOWN-HAZARDS attach-lifecycle invariant, satisfied by\n//! construction). The broker session is labeled with the endpoint id so\n//! `spt rc <id>` resolves exactly this PTY.\n//!\n//! **Id plumbing.** The endpoint id reaches the harness two ways, both filled\n//! from the SAME `{key}` catalog: argv via `{id}` substitution into the opaque\n//! `[session.self]` command (the harness parses `--id`), AND the manifest\n//! `[env.<VAR>]` direction = inject values (e.g. `SPT_ENDPOINT_ID = \"{id}\"`),\n//! which are `{key}`-substituted and passed in `SpawnReq.env` so the broker sets\n//! them on the spawned child (REQ-HAZARD-ENV-SUBST). A flagless harness whose\n//! `[session.self]` has no argv slot for `{id}` (e.g. bare `claude`) relies on\n//! the env inject — which previously arrived EMPTY (the `[env]` value was never\n//! substituted nor applied), so the harness seeded-by-PPID instead of binding →\n//! a zero-perch `NO_PERCH`. This mirrors [`crate::shellhost::launch_shell_brokered_in`]\n//! but feeds `[session.self]` (not `[shell].spawn`), uses real terminal PTY dims,\n//! and mints no link token (a harness binds via `api`, not the shell link\n//! handshake).\n// [impl->REQ-HOST-RUN-1]\n\nuse std::collections::BTreeMap;\n\nuse spt_runtime::manifest::{AdapterKind, EnvDirection, Manifest};\n\nuse crate::brain::{now_ms, Brain};\nuse crate::msg::SpawnReq;\n\n/// Default PTY geometry for a freshly brought-up harness, before an attach\n/// resizes it to the operator's real terminal. A harness *is* a terminal (unlike\n/// a shell's wide anti-wrap stdin conduit), so a conventional 80×24 is right.\npub const DEFAULT_HARNESS_ROWS: u16 = 24;\npub const DEFAULT_HARNESS_COLS: u16 = 80;\n\n/// Outcome of an spt-hosted harness bringup: the broker session id (the attach\n/// handle) plus the OS pid of the spawned harness (`None` if the broker backend\n/// exposed none).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub struct HarnessSpawned {\n    pub session_id: u64,\n    pub pid: Option<u32>,\n}\n\n/// Mint a provisional spawn-time session id (16 hex chars). The *real* session id\n/// is reported post-spawn by the harness (`identity.session_id_source =\n/// \"post_spawn\"`); this only fills the `{session_id}` placeholder at spawn so the\n/// command template resolves. A fresh create mints one; `--resume` supplies the\n/// prior session id instead.\npub fn mint_session_id() -> String {\n    let seed = spt_proto::identity::Identity::generate().seed();\n    seed[..8].iter().map(|b| format!(\"{b:02x}\")).collect()\n}\n\n/// True when `s` has the shape of a spt-core spawn PROVISIONAL session id — the\n/// [`mint_session_id`] format: exactly 16 LOWERCASE hex chars. A PostSpawn\n/// adapter's real harness session id (e.g. Claude's 36-char dashed UUID) never\n/// matches. This is spt-core recognizing its OWN mint format — NOT adapter-\n/// specific knowledge — used as the B2 defensive belt so a provisional never\n/// leaks into a resume command (`claude -r <provisional>` hangs the harness at a\n/// \"no sessions match\" picker; REQ-RESUME-HARNESS-SESSION-ID).\npub fn is_provisional_session_id(s: &str) -> bool {\n    s.len() == 16\n        && s.bytes()\n            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))\n}\n\n/// Validate the adapter and fill its session command for one endpoint, returning\n/// the tokenized `program + args`. Pure (no spawn) — the broker call in\n/// [`launch_harness_brokered_in`] is the only side effect. Fails closed: a\n/// non-harness adapter, a missing role command, an unresolved `{placeholder}`,\n/// or an empty command all error before anything is spawned.\n///\n/// `is_resume`: select the NATIVE-resume role over the fresh one — when `true`\n/// AND the adapter declares `[session.resume]`, the resume template is filled\n/// (the harness runs e.g. `claude -r {session_id}` instead of a fresh\n/// `claude --session-id {session_id}`, which would start a blank transcript). A\n/// `true` with NO `[session.resume]` declared FALLS BACK to `[session.self]`\n/// (full back-compat); a `false` always uses `[session.self]`.\n// [impl->REQ-HOST-RUN-1]\n// [impl->REQ-SESSION-RESUME-TEMPLATE]\npub fn prepare_harness_spawn(\n    endpoint_id: &str,\n    adapter_name: &str,\n    session_id: &str,\n    manifest: &Manifest,\n    is_resume: bool,\n    // The adapter install dir (record `source_dir`) — fills `{adapter_dir}` and\n    // resolves the translation binary's program token against it (ADR-0029 /\n    // REQ-INSTALL-11). `None` keeps PATH-only resolution + an unset `{adapter_dir}`.\n    install_dir: Option<&str>,\n    // This node's advertised label for the `{node}` fill (REQ-MANIFEST-NODE-KEY),\n    // ALREADY resolved by the caller via `hostlabel::node_fill_label` (daemon in-mem\n    // label → OS-hostname fallback). `None` when no label is known — `{node}` is then\n    // left UNFILLED so a manifest referencing it fails LOUD (never an empty token).\n    node_label: Option<&str>,\n) -> Result<PreparedSpawn, String> {\n    if manifest.adapter.kind != AdapterKind::Harness {\n        return Err(format!(\n            \"adapter '{adapter_name}' is kind={:?}, not a harness — \\\n             `spt endpoint run` brings up harness endpoints\",\n            manifest.adapter.kind\n        ));\n    }\n    // Resume selects `[session.resume]` when declared, else falls back to\n    // `[session.self]` (an adapter with no resume sibling behaves exactly as\n    // today). A fresh bringup always uses `[session.self]`.\n    let role = if is_resume {\n        manifest\n            .session\n            .resume\n            .as_ref()\n            .or(manifest.session.self_.as_ref())\n    } else {\n        manifest.session.self_.as_ref()\n    }\n    .ok_or_else(|| {\n        format!(\"adapter '{adapter_name}' declares no [session.self] — nothing to bring up\")\n    })?;\n    // The guaranteed-fill catalog for a self-spawn. `id` rides argv so the harness\n    // binds to exactly this endpoint; `session_name` defaults to the id. The SAME\n    // catalog fills the [env] inject values (REQ-HAZARD-ENV-SUBST).\n    let mut keys = BTreeMap::from([\n        (\"id\".to_string(), endpoint_id.to_string()),\n        (\"adapter_name\".to_string(), adapter_name.to_string()),\n        (\"session_id\".to_string(), session_id.to_string()),\n        (\"session_name\".to_string(), endpoint_id.to_string()),\n    ]);\n    // `{node}` (REQ-MANIFEST-NODE-KEY): insert ONLY when a label is known. Absent →\n    // the key is unfilled, so a `{node}` reference fails LOUD via `fill_template`'s\n    // MissingKey (never an empty argv element — the tokenize-then-fill guarantee).\n    if let Some(node) = node_label.filter(|s| !s.is_empty()) {\n        keys.insert(\"node\".to_string(), node.to_string());\n    }\n    // [impl->REQ-MANIFEST-SUBST] add `{adapter_dir}` (and keep `{adapter_name}`)\n    // so the session argv + [env] inject values can point at the adapter's own\n    // packed binary/resource. `or_insert` semantics: never clobbers the above.\n    spt_runtime::runtime::inject_adapter_keys(\n        &mut keys,\n        install_dir.map(std::path::Path::new),\n        Some(adapter_name),\n    );\n    // [impl->REQ-HAZARD-TEMPLATE-ARGV-FILL] tokenize the template then fill each\n    // token so a multi-word/quote/semicolon {key} value is one argv element.\n    let tokens =\n        spt_runtime::runtime::fill_template_tokens(&role.command, &keys).map_err(|e| e.to_string())?;\n    if tokens.is_empty() {\n        return Err(\"empty session command\".into());\n    }\n    // [impl->REQ-HAZARD-ENV-SUBST] every [env.<VAR>] direction=inject value is\n    // {key}-substituted from the SAME catalog and passed to the broker for the\n    // spawned child's environment — the schema's \"Value to inject (with\n    // substitution)\" promise, previously unfulfilled (the value arrived empty, so\n    // a flagless harness seeded-by-PPID instead of binding → zero perch).\n    let mut env = BTreeMap::new();\n    for (name, var) in &manifest.env {\n        if var.direction == EnvDirection::Inject {\n            // Validation guarantees an inject directive has a value; fill it.\n            if let Some(template) = &var.value {\n                let filled = spt_runtime::runtime::fill_template(template, &keys)\n                    .map_err(|e| e.to_string())?;\n                env.insert(name.clone(), filled);\n            }\n        }\n    }\n    // ADR-0022 / W2 / ADR-0029: carry the adapter's idle-delivery translation\n    // binary as a resolved argv (program + args) so the broker can lifecycle-manage\n    // it alongside this session. `command` (preferred) or the deprecated `path`;\n    // adapter-static keys substituted + the program token resolved against the\n    // install dir (REQ-INSTALL-11).\n    // [impl->REQ-TRANSLATE-COMMAND]\n    let translation_binary = manifest\n        .message_idle_translation_binary\n        .as_ref()\n        .and_then(|decl| {\n…\n        });\n…\n}\n\n[Showing lines 1-183 of 648. Use :184 to continue]"}],"details":{"truncation":{"content":"//! Harness-instance hosting (M12-W1 — spt-hosted harness bringup, REQ-HOST-RUN-1):\r\n//! the spt-HOSTED counterpart to harness-hosted launch.\r\n//!\r\n//! **Who spawns the harness.** Today a harness is *harness-hosted*: an external\r\n//! launcher runs the adapter's `[session.self]` command and the harness binds its\r\n//! own perch on startup (`api listen`/`api bind` → `establish_perch`). spt-hosting\r\n//! changes *only who owns the PTY*: the daemon spawns `[session.self]` into a\r\n//! **broker-held PTY** ([`launch_harness_brokered_in`]). The harness still\r\n//! self-registers its perch on bind — there is no daemon pre-register step.\r\n//!\r\n//! **Why broker-held.** The broker owns the PTY, so the harness survives a\r\n//! `spt rc` detach — PTY ownership stays with the broker, no termination on\r\n//! detach (the KNOWN-HAZARDS attach-lifecycle invariant, satisfied by\r\n//! construction). The broker session is labeled with the endpoint id so\r\n//! `spt rc <id>` resolves exactly this PTY.\r\n//!\r\n//! **Id plumbing.** The endpoint id reaches the harness two ways, both filled\r\n//! from the SAME `{key}` catalog: argv via `{id}` substitution into the opaque\r\n//! `[session.self]` command (the harness parses `--id`), AND the manifest\r\n//! `[env.<VAR>]` direction = inject values (e.g. `SPT_ENDPOINT_ID = \"{id}\"`),\r\n//! which are `{key}`-substituted and passed in `SpawnReq.env` so the broker sets\r\n//! them on the spawned child (REQ-HAZARD-ENV-SUBST). A flagless harness whose\r\n//! `[session.self]` has no argv slot for `{id}` (e.g. bare `claude`) relies on\r\n//! the env inject — which previously arrived EMPTY (the `[env]` value was never\r\n//! substituted nor applied), so the harness seeded-by-PPID instead of binding →\r\n//! a zero-perch `NO_PERCH`. This mirrors [`crate::shellhost::launch_shell_brokered_in`]\r\n//! but feeds `[session.self]` (not `[shell].spawn`), uses real terminal PTY dims,\r\n//! and mints no link token (a harness binds via `api`, not the shell link\r\n//! handshake).\r\n// [impl->REQ-HOST-RUN-1]\r\n\r\nuse std::collections::BTreeMap;\r\n\r\nuse spt_runtime::manifest::{AdapterKind, EnvDirection, Manifest};\r\n\r\nuse crate::brain::{now_ms, Brain};\r\nuse crate::msg::SpawnReq;\r\n\r\n/// Default PTY geometry for a freshly brought-up harness, before an attach\r\n/// resizes it to the operator's real terminal. A harness *is* a terminal (unlike\r\n/// a shell's wide anti-wrap stdin conduit), so a conventional 80×24 is right.\r\npub const DEFAULT_HARNESS_ROWS: u16 = 24;\r\npub const DEFAULT_HARNESS_COLS: u16 = 80;\r\n\r\n/// Outcome of an spt-hosted harness bringup: the broker session id (the attach\r\n/// handle) plus the OS pid of the spawned harness (`None` if the broker backend\r\n/// exposed none).\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub struct HarnessSpawned {\r\n    pub session_id: u64,\r\n    pub pid: Option<u32>,\r\n}\r\n\r\n/// Mint a provisional spawn-time session id (16 hex chars). The *real* session id\r\n/// is reported post-spawn by the harness (`identity.session_id_source =\r\n/// \"post_spawn\"`); this only fills the `{session_id}` placeholder at spawn so the\r\n/// command template resolves. A fresh create mints one; `--resume` supplies the\r\n/// prior session id instead.\r\npub fn mint_session_id() -> String {\r\n    let seed = spt_proto::identity::Identity::generate().seed();\r\n    seed[..8].iter().map(|b| format!(\"{b:02x}\")).collect()\r\n}\r\n\r\n/// True when `s` has the shape of a spt-core spawn PROVISIONAL session id — the\r\n/// [`mint_session_id`] format: exactly 16 LOWERCASE hex chars. A PostSpawn\r\n/// adapter's real harness session id (e.g. Claude's 36-char dashed UUID) never\r\n/// matches. This is spt-core recognizing its OWN mint format — NOT adapter-\r\n/// specific knowledge — used as the B2 defensive belt so a provisional never\r\n/// leaks into a resume command (`claude -r <provisional>` hangs the harness at a\r\n/// \"no sessions match\" picker; REQ-RESUME-HARNESS-SESSION-ID).\r\npub fn is_provisional_session_id(s: &str) -> bool {\r\n    s.len() == 16\r\n        && s.bytes()\r\n            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))\r\n}\r\n\r\n/// Validate the adapter and fill its session command for one endpoint, returning\r\n/// the tokenized `program + args`. Pure (no spawn) — the broker call in\r\n/// [`launch_harness_brokered_in`] is the only side effect. Fails closed: a\r\n/// non-harness adapter, a missing role command, an unresolved `{placeholder}`,\r\n/// or an empty command all error before anything is spawned.\r\n///\r\n/// `is_resume`: select the NATIVE-resume role over the fresh one — when `true`\r\n/// AND the adapter declares `[session.resume]`, the resume template is filled\r\n/// (the harness runs e.g. `claude -r {session_id}` instead of a fresh\r\n/// `claude --session-id {session_id}`, which would start a blank transcript). A\r\n/// `true` with NO `[session.resume]` declared FALLS BACK to `[session.self]`\r\n/// (full back-compat); a `false` always uses `[session.self]`.\r\n// [impl->REQ-HOST-RUN-1]\r\n// [impl->REQ-SESSION-RESUME-TEMPLATE]\r\npub fn prepare_harness_spawn(\r\n    endpoint_id: &str,\r\n    adapter_name: &str,\r\n    session_id: &str,\r\n    manifest: &Manifest,\r\n    is_resume: bool,\r\n    // The adapter install dir (record `source_dir`) — fills `{adapter_dir}` and\r\n    // resolves the translation binary's program token against it (ADR-0029 /\r\n    // REQ-INSTALL-11). `None` keeps PATH-only resolution + an unset `{adapter_dir}`.\r\n    install_dir: Option<&str>,\r\n    // This node's advertised label for the `{node}` fill (REQ-MANIFEST-NODE-KEY),\r\n    // ALREADY resolved by the caller via `hostlabel::node_fill_label` (daemon in-mem\r\n    // label → OS-hostname fallback). `None` when no label is known — `{node}` is then\r\n    // left UNFILLED so a manifest referencing it fails LOUD (never an empty token).\r\n    node_label: Option<&str>,\r\n) -> Result<PreparedSpawn, String> {\r\n    if manifest.adapter.kind != AdapterKind::Harness {\r\n        return Err(format!(\r\n            \"adapter '{adapter_name}' is kind={:?}, not a harness — \\\r\n             `spt endpoint run` brings up harness endpoints\",\r\n            manifest.adapter.kind\r\n        ));\r\n    }\r\n    // Resume selects `[session.resume]` when declared, else falls back to\r\n    // `[session.self]` (an adapter with no resume sibling behaves exactly as\r\n    // today). A fresh bringup always uses `[session.self]`.\r\n    let role = if is_resume {\r\n        manifest\r\n            .session\r\n            .resume\r\n            .as_ref()\r\n            .or(manifest.session.self_.as_ref())\r\n    } else {\r\n        manifest.session.self_.as_ref()\r\n    }\r\n    .ok_or_else(|| {\r\n        format!(\"adapter '{adapter_name}' declares no [session.self] — nothing to bring up\")\r\n    })?;\r\n    // The guaranteed-fill catalog for a self-spawn. `id` rides argv so the harness\r\n    // binds to exactly this endpoint; `session_name` defaults to the id. The SAME\r\n    // catalog fills the [env] inject values (REQ-HAZARD-ENV-SUBST).\r\n    let mut keys = BTreeMap::from([\r\n        (\"id\".to_string(), endpoint_id.to_string()),\r\n        (\"adapter_name\".to_string(), adapter_name.to_string()),\r\n        (\"session_id\".to_string(), session_id.to_string()),\r\n        (\"session_name\".to_string(), endpoint_id.to_string()),\r\n    ]);\r\n    // `{node}` (REQ-MANIFEST-NODE-KEY): insert ONLY when a label is known. Absent →\r\n    // the key is unfilled, so a `{node}` reference fails LOUD via `fill_template`'s\r\n    // MissingKey (never an empty argv element — the tokenize-then-fill guarantee).\r\n    if let Some(node) = node_label.filter(|s| !s.is_empty()) {\r\n        keys.insert(\"node\".to_string(), node.to_string());\r\n    }\r\n    // [impl->REQ-MANIFEST-SUBST] add `{adapter_dir}` (and keep `{adapter_name}`)\r\n    // so the session argv + [env] inject values can point at the adapter's own\r\n    // packed binary/resource. `or_insert` semantics: never clobbers the above.\r\n    spt_runtime::runtime::inject_adapter_keys(\r\n        &mut keys,\r\n        install_dir.map(std::path::Path::new),\r\n        Some(adapter_name),\r\n    );\r\n    // [impl->REQ-HAZARD-TEMPLATE-ARGV-FILL] tokenize the template then fill each\r\n    // token so a multi-word/quote/semicolon {key} value is one argv element.\r\n    let tokens =\r\n        spt_runtime::runtime::fill_template_tokens(&role.command, &keys).map_err(|e| e.to_string())?;\r\n    if tokens.is_empty() {\r\n        return Err(\"empty session command\".into());\r\n    }\r\n    // [impl->REQ-HAZARD-ENV-SUBST] every [env.<VAR>] direction=inject value is\r\n    // {key}-substituted from the SAME catalog and passed to the broker for the\r\n    // spawned child's environment — the schema's \"Value to inject (with\r\n    // substitution)\" promise, previously unfulfilled (the value arrived empty, so\r\n    // a flagless harness seeded-by-PPID instead of binding → zero perch).\r\n    let mut env = BTreeMap::new();\r\n    for (name, var) in &manifest.env {\r\n        if var.direction == EnvDirection::Inject {\r\n            // Validation guarantees an inject directive has a value; fill it.\r\n            if let Some(template) = &var.value {\r\n                let filled = spt_runtime::runtime::fill_template(template, &keys)\r\n                    .map_err(|e| e.to_string())?;\r\n                env.insert(name.clone(), filled);\r\n            }\r\n        }\r\n    }\r\n    // ADR-0022 / W2 / ADR-0029: carry the adapter's idle-delivery translation\r\n    // binary as a resolved argv (program + args) so the broker can lifecycle-manage\r\n    // it alongside this session. `command` (preferred) or the deprecated `path`;\r\n    // adapter-static keys substituted + the program token resolved against the\r\n    // install dir (REQ-INSTALL-11).\r\n    // [impl->REQ-TRANSLATE-COMMAND]\r\n    let translation_binary = manifest\r\n        .message_idle_translation_binary\r\n        .as_ref()\r","truncated":true,"truncatedBy":"lines","totalLines":648,"totalBytes":9767,"outputLines":183,"outputBytes":9767,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! Harness-instance hosting (M12-W1 — spt-hosted harness bringup, REQ-HOST-RUN-1):\r\n//! the spt-HOSTED counterpart to harness-hosted launch.\r\n//!\r\n//! **Who spawns the harness.** Today a harness is *harness-hosted*: an external\r\n//! launcher runs the adapter's `[session.self]` command and the harness binds its\r\n//! own perch on startup (`api listen`/`api bind` → `establish_perch`). spt-hosting\r\n//! changes *only who owns the PTY*: the daemon spawns `[session.self]` into a\r\n//! **broker-held PTY** ([`launch_harness_brokered_in`]). The harness still\r\n//! self-registers its perch on bind — there is no daemon pre-register step.\r\n//!\r\n//! **Why broker-held.** The broker owns the PTY, so the harness survives a\r\n//! `spt rc` detach — PTY ownership stays with the broker, no termination on\r\n//! detach (the KNOWN-HAZARDS attach-lifecycle invariant, satisfied by\r\n//! construction). The broker session is labeled with the endpoint id so\r\n//! `spt rc <id>` resolves exactly this PTY.\r\n//!\r\n//! **Id plumbing.** The endpoint id reaches the harness two ways, both filled\r\n//! from the SAME `{key}` catalog: argv via `{id}` substitution into the opaque\r\n//! `[session.self]` command (the harness parses `--id`), AND the manifest\r\n//! `[env.<VAR>]` direction = inject values (e.g. `SPT_ENDPOINT_ID = \"{id}\"`),\r\n//! which are `{key}`-substituted and passed in `SpawnReq.env` so the broker sets\r\n//! them on the spawned child (REQ-HAZARD-ENV-SUBST). A flagless harness whose\r\n//! `[session.self]` has no argv slot for `{id}` (e.g. bare `claude`) relies on\r\n//! the env inject — which previously arrived EMPTY (the `[env]` value was never\r\n//! substituted nor applied), so the harness seeded-by-PPID instead of binding →\r\n//! a zero-perch `NO_PERCH`. This mirrors [`crate::shellhost::launch_shell_brokered_in`]\r\n//! but feeds `[session.self]` (not `[shell].spawn`), uses real terminal PTY dims,\r\n//! and mints no link token (a harness binds via `api`, not the shell link\r\n//! handshake).\r\n// [impl->REQ-HOST-RUN-1]\r\n\r\nuse std::collections::BTreeMap;\r\n\r\nuse spt_runtime::manifest::{AdapterKind, EnvDirection, Manifest};\r\n\r\nuse crate::brain::{now_ms, Brain};\r\nuse crate::msg::SpawnReq;\r\n\r\n/// Default PTY geometry for a freshly brought-up harness, before an attach\r\n/// resizes it to the operator's real terminal. A harness *is* a terminal (unlike\r\n/// a shell's wide anti-wrap stdin conduit), so a conventional 80×24 is right.\r\npub const DEFAULT_HARNESS_ROWS: u16 = 24;\r\npub const DEFAULT_HARNESS_COLS: u16 = 80;\r\n\r\n/// Outcome of an spt-hosted harness bringup: the broker session id (the attach\r\n/// handle) plus the OS pid of the spawned harness (`None` if the broker backend\r\n/// exposed none).\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub struct HarnessSpawned {\r\n    pub session_id: u64,\r\n    pub pid: Option<u32>,\r\n}\r\n\r\n/// Mint a provisional spawn-time session id (16 hex chars). The *real* session id\r\n/// is reported post-spawn by the harness (`identity.session_id_source =\r\n/// \"post_spawn\"`); this only fills the `{session_id}` placeholder at spawn so the\r\n/// command template resolves. A fresh create mints one; `--resume` supplies the\r\n/// prior session id instead.\r\npub fn mint_session_id() -> String {\r\n    let seed = spt_proto::identity::Identity::generate().seed();\r\n    seed[..8].iter().map(|b| format!(\"{b:02x}\")).collect()\r\n}\r\n\r\n/// True when `s` has the shape of a spt-core spawn PROVISIONAL session id — the\r\n/// [`mint_session_id`] format: exactly 16 LOWERCASE hex chars. A PostSpawn\r\n/// adapter's real harness session id (e.g. Claude's 36-char dashed UUID) never\r\n/// matches. This is spt-core recognizing its OWN mint format — NOT adapter-\r\n/// specific knowledge — used as the B2 defensive belt so a provisional never\r\n/// leaks into a resume command (`claude -r <provisional>` hangs the harness at a\r\n/// \"no sessions match\" picker; REQ-RESUME-HARNESS-SESSION-ID).\r\npub fn is_provisional_session_id(s: &str) -> bool {\r\n    s.len() == 16\r\n        && s.bytes()\r\n            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))\r\n}\r\n\r\n/// Validate the adapter and fill its session command for one endpoint, returning\r\n/// the tokenized `program + args`. Pure (no spawn) — the broker call in\r\n/// [`launch_harness_brokered_in`] is the only side effect. Fails closed: a\r\n/// non-harness adapter, a missing role command, an unresolved `{placeholder}`,\r\n/// or an empty command all error before anything is spawned.\r\n///\r\n/// `is_resume`: select the NATIVE-resume role over the fresh one — when `true`\r\n/// AND the adapter declares `[session.resume]`, the resume template is filled\r\n/// (the harness runs e.g. `claude -r {session_id}` instead of a fresh\r\n/// `claude --session-id {session_id}`, which would start a blank transcript). A\r\n/// `true` with NO `[session.resume]` declared FALLS BACK to `[session.self]`\r\n/// (full back-compat); a `false` always uses `[session.self]`.\r\n// [impl->REQ-HOST-RUN-1]\r\n// [impl->REQ-SESSION-RESUME-TEMPLATE]\r\npub fn prepare_harness_spawn(\r\n    endpoint_id: &str,\r\n    adapter_name: &str,\r\n    session_id: &str,\r\n    manifest: &Manifest,\r\n    is_resume: bool,\r\n    // The adapter install dir (record `source_dir`) — fills `{adapter_dir}` and\r\n    // resolves the translation binary's program token against it (ADR-0029 /\r\n    // REQ-INSTALL-11). `None` keeps PATH-only resolution + an unset `{adapter_dir}`.\r\n    install_dir: Option<&str>,\r\n    // This node's advertised label for the `{node}` fill (REQ-MANIFEST-NODE-KEY),\r\n    // ALREADY resolved by the caller via `hostlabel::node_fill_label` (daemon in-mem\r\n    // label → OS-hostname fallback). `None` when no label is known — `{node}` is then\r\n    // left UNFILLED so a manifest referencing it fails LOUD (never an empty token).\r\n    node_label: Option<&str>,\r\n) -> Result<PreparedSpawn, String> {\r\n    if manifest.adapter.kind != AdapterKind::Harness {\r\n        return Err(format!(\r\n            \"adapter '{adapter_name}' is kind={:?}, not a harness — \\\r\n             `spt endpoint run` brings up harness endpoints\",\r\n            manifest.adapter.kind\r\n        ));\r\n    }\r\n    // Resume selects `[session.resume]` when declared, else falls back to\r\n    // `[session.self]` (an adapter with no resume sibling behaves exactly as\r\n    // today). A fresh bringup always uses `[session.self]`.\r\n    let role = if is_resume {\r\n        manifest\r\n            .session\r\n            .resume\r\n            .as_ref()\r\n            .or(manifest.session.self_.as_ref())\r\n    } else {\r\n        manifest.session.self_.as_ref()\r\n    }\r\n    .ok_or_else(|| {\r\n        format!(\"adapter '{adapter_name}' declares no [session.self] — nothing to bring up\")\r\n    })?;\r\n    // The guaranteed-fill catalog for a self-spawn. `id` rides argv so the harness\r\n    // binds to exactly this endpoint; `session_name` defaults to the id. The SAME\r\n    // catalog fills the [env] inject values (REQ-HAZARD-ENV-SUBST).\r\n    let mut keys = BTreeMap::from([\r\n        (\"id\".to_string(), endpoint_id.to_string()),\r\n        (\"adapter_name\".to_string(), adapter_name.to_string()),\r\n        (\"session_id\".to_string(), session_id.to_string()),\r\n        (\"session_name\".to_string(), endpoint_id.to_string()),\r\n    ]);\r\n    // `{node}` (REQ-MANIFEST-NODE-KEY): insert ONLY when a label is known. Absent →\r\n    // the key is unfilled, so a `{node}` reference fails LOUD via `fill_template`'s\r\n    // MissingKey (never an empty argv element — the tokenize-then-fill guarantee).\r\n    if let Some(node) = node_label.filter(|s| !s.is_empty()) {\r\n        keys.insert(\"node\".to_string(), node.to_string());\r\n    }\r\n    // [impl->REQ-MANIFEST-SUBST] add `{adapter_dir}` (and keep `{adapter_name}`)\r\n    // so the session argv + [env] inject values can point at the adapter's own\r\n    // packed binary/resource. `or_insert` semantics: never clobbers the above.\r\n    spt_runtime::runtime::inject_adapter_keys(\r\n        &mut keys,\r\n        install_dir.map(std::path::Path::new),\r\n        Some(adapter_name),\r\n    );\r\n    // [impl->REQ-HAZARD-TEMPLATE-ARGV-FILL] tokenize the template then fill each\r\n    // token so a multi-word/quote/semicolon {key} value is one argv element.\r\n    let tokens =\r\n        spt_runtime::runtime::fill_template_tokens(&role.command, &keys).map_err(|e| e.to_string())?;\r\n    if tokens.is_empty() {\r\n        return Err(\"empty session command\".into());\r\n    }\r\n    // [impl->REQ-HAZARD-ENV-SUBST] every [env.<VAR>] direction=inject value is\r\n    // {key}-substituted from the SAME catalog and passed to the broker for the\r\n    // spawned child's environment — the schema's \"Value to inject (with\r\n    // substitution)\" promise, previously unfulfilled (the value arrived empty, so\r\n    // a flagless harness seeded-by-PPID instead of binding → zero perch).\r\n    let mut env = BTreeMap::new();\r\n    for (name, var) in &manifest.env {\r\n        if var.direction == EnvDirection::Inject {\r\n            // Validation guarantees an inject directive has a value; fill it.\r\n            if let Some(template) = &var.value {\r\n                let filled = spt_runtime::runtime::fill_template(template, &keys)\r\n                    .map_err(|e| e.to_string())?;\r\n                env.insert(name.clone(), filled);\r\n            }\r\n        }\r\n    }\r\n    // ADR-0022 / W2 / ADR-0029: carry the adapter's idle-delivery translation\r\n    // binary as a resolved argv (program + args) so the broker can lifecycle-manage\r\n    // it alongside this session. `command` (preferred) or the deprecated `path`;\r\n    // adapter-static keys substituted + the program token resolved against the\r\n    // install dir (REQ-INSTALL-11).\r\n    // [impl->REQ-TRANSLATE-COMMAND]\r\n    let translation_binary = manifest\r\n        .message_idle_translation_binary\r\n        .as_ref()\r\n        .and_then(|decl| {\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,null,190,null,196]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\harnesshost.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":648,"totalBytes":9767,"outputLines":183,"outputBytes":9767,"shownRange":{"start":1,"end":183},"nextOffset":184}}},"isError":false,"timestamp":1784197504289}}
{"type":"message","id":"2b797f20","parentId":"f9bd6a6f","timestamp":"2026-07-16T10:25:04.289Z","message":{"role":"toolResult","toolCallId":"call_Nbz49jT1uncFQj5raBoVW8nj|fc_017bff223290b7ba016a58b1803d34819ab22d49ed8e2a2a2d","toolName":"read","content":[{"type":"text","text":"//! De-elevation seam for daemon spawn (KNOWN-HAZARDS 5.7).\n//!\n//! Membership-implies-reachability made every `spt` invocation a potential\n//! daemon spawner (`ensure_running`) — including the elevation-gated ones\n//! (`subnet create`/`join`, REQ-SUBNET-4). A daemon spawned with the\n//! elevated token is poison: on Windows its named pipes deny unelevated\n//! clients (every later `spt` reads not-running → spawn → bind\n//! Access-denied); on Linux a sudo'd spawn roots the daemon and — because\n//! sudo flips `$HOME` — can mint a *different node identity* in root's\n//! universe (the key-flip behind the 4.10 ghost rows).\n//!\n//! The invariant: **the daemon always runs unelevated in the invoking\n//! user's universe, regardless of which command spawns it.** Two\n//! enforcement points consume this module:\n//!\n//! - [`crate::daemon::spawn_detached`] de-elevates the child it launches —\n//!   Windows: the UAC **linked token** ([`spawn_deelevated`]); Linux: drop\n//!   the child to `SUDO_UID`/`SUDO_GID` with the invoker's `HOME`\n//!   ([`sudo_invoker`] + `CommandExt::uid/gid`).\n//! - The [`crate::daemon::Daemon::run`] entry guard catches a *directly*\n//!   elevated `spt daemon` — Linux drops privileges in-process\n//!   ([`SudoInvoker::drop_in_process`]) before touching any state; Windows\n//!   respawns de-elevated and exits.\n//!\n//! When no unelevated identity exists to drop to (UAC disabled, genuine\n//! root login, SYSTEM), callers run as-is with a loud warning — a\n//! consistent universe, never a torn one. Elevated one-shot *clients*\n//! talking downward to an unelevated daemon are fine; the daemon side is\n//! the invariant.\n//!\n//! The OS probes mirror `spt`'s REQ-PAIR-6 `elevation.rs` (the CLI gate);\n//! they are re-declared here because the dependency arrow points the other\n//! way (`spt` → `spt-daemon`). Like [`crate::daemon::detached_no_inherit`],\n//! the Windows calls are raw `extern \"system\"` declarations (advapi32 is in\n//! the MSVC default link set) — no new dependency for one seam.\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\n\n#[cfg(unix)]\npub use unix::{daemon_target_user, sudo_invoker, SudoInvoker};\n\n#[cfg(windows)]\npub use windows::{has_deelevation_target, is_elevated, spawn_deelevated};\n\n/// What a spawner should do about elevation — the pure decision seam (the\n/// REQ-PAIR-6 `gate_for` posture: the OS probe is environment-dependent,\n/// the decision is unit-tested both ways).\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum SpawnPlan {\n    /// Not elevated (or no de-elevation target exists): spawn as-is.\n    Inherit,\n    /// Elevated with an unelevated identity available: spawn de-elevated.\n    DeElevate,\n}\n\n/// The pure decision: de-elevate iff the process is elevated AND an\n/// unelevated identity exists to drop to (Windows: the UAC linked token;\n/// Linux: a parsed `SUDO_UID`). Elevated-without-target spawns `Inherit`\n/// — a consistent elevated universe with a loud warning beats a torn one.\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\npub fn plan_spawn(elevated: bool, has_deelevation_target: bool) -> SpawnPlan {\n    if elevated && has_deelevation_target {\n        SpawnPlan::DeElevate\n    } else {\n        SpawnPlan::Inherit\n    }\n}\n\n/// Overlay `overrides` onto a Windows environment block (`NAME=VALUE\\0`\n/// UTF-16 strings ended by a final `\\0`). Entries whose name matches an\n/// override (case-insensitively, as Windows env names are) are replaced;\n/// unmatched overrides are appended. Returns a fresh, double-null-terminated\n/// block.\n///\n/// This is the seam that keeps an explicit `SPT_HOME` (the universe\n/// relocation knob — `spt_store::perch::spt_home`) alive across the Windows\n/// de-elevation respawn. `CreateEnvironmentBlock` rebuilds the desktop\n/// user's variables from scratch (to fix `%LOCALAPPDATA%` when UAC elevated\n/// through a different admin account), which would otherwise DROP the\n\n[Showing lines 1-78 of 963. Use :79 to continue]"}],"details":{"truncation":{"content":"//! De-elevation seam for daemon spawn (KNOWN-HAZARDS 5.7).\r\n//!\r\n//! Membership-implies-reachability made every `spt` invocation a potential\r\n//! daemon spawner (`ensure_running`) — including the elevation-gated ones\r\n//! (`subnet create`/`join`, REQ-SUBNET-4). A daemon spawned with the\r\n//! elevated token is poison: on Windows its named pipes deny unelevated\r\n//! clients (every later `spt` reads not-running → spawn → bind\r\n//! Access-denied); on Linux a sudo'd spawn roots the daemon and — because\r\n//! sudo flips `$HOME` — can mint a *different node identity* in root's\r\n//! universe (the key-flip behind the 4.10 ghost rows).\r\n//!\r\n//! The invariant: **the daemon always runs unelevated in the invoking\r\n//! user's universe, regardless of which command spawns it.** Two\r\n//! enforcement points consume this module:\r\n//!\r\n//! - [`crate::daemon::spawn_detached`] de-elevates the child it launches —\r\n//!   Windows: the UAC **linked token** ([`spawn_deelevated`]); Linux: drop\r\n//!   the child to `SUDO_UID`/`SUDO_GID` with the invoker's `HOME`\r\n//!   ([`sudo_invoker`] + `CommandExt::uid/gid`).\r\n//! - The [`crate::daemon::Daemon::run`] entry guard catches a *directly*\r\n//!   elevated `spt daemon` — Linux drops privileges in-process\r\n//!   ([`SudoInvoker::drop_in_process`]) before touching any state; Windows\r\n//!   respawns de-elevated and exits.\r\n//!\r\n//! When no unelevated identity exists to drop to (UAC disabled, genuine\r\n//! root login, SYSTEM), callers run as-is with a loud warning — a\r\n//! consistent universe, never a torn one. Elevated one-shot *clients*\r\n//! talking downward to an unelevated daemon are fine; the daemon side is\r\n//! the invariant.\r\n//!\r\n//! The OS probes mirror `spt`'s REQ-PAIR-6 `elevation.rs` (the CLI gate);\r\n//! they are re-declared here because the dependency arrow points the other\r\n//! way (`spt` → `spt-daemon`). Like [`crate::daemon::detached_no_inherit`],\r\n//! the Windows calls are raw `extern \"system\"` declarations (advapi32 is in\r\n//! the MSVC default link set) — no new dependency for one seam.\r\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\r\n\r\n#[cfg(unix)]\r\npub use unix::{daemon_target_user, sudo_invoker, SudoInvoker};\r\n\r\n#[cfg(windows)]\r\npub use windows::{has_deelevation_target, is_elevated, spawn_deelevated};\r\n\r\n/// What a spawner should do about elevation — the pure decision seam (the\r\n/// REQ-PAIR-6 `gate_for` posture: the OS probe is environment-dependent,\r\n/// the decision is unit-tested both ways).\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub enum SpawnPlan {\r\n    /// Not elevated (or no de-elevation target exists): spawn as-is.\r\n    Inherit,\r\n    /// Elevated with an unelevated identity available: spawn de-elevated.\r\n    DeElevate,\r\n}\r\n\r\n/// The pure decision: de-elevate iff the process is elevated AND an\r\n/// unelevated identity exists to drop to (Windows: the UAC linked token;\r\n/// Linux: a parsed `SUDO_UID`). Elevated-without-target spawns `Inherit`\r\n/// — a consistent elevated universe with a loud warning beats a torn one.\r\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\r\npub fn plan_spawn(elevated: bool, has_deelevation_target: bool) -> SpawnPlan {\r\n    if elevated && has_deelevation_target {\r\n        SpawnPlan::DeElevate\r\n    } else {\r\n        SpawnPlan::Inherit\r\n    }\r\n}\r\n\r\n/// Overlay `overrides` onto a Windows environment block (`NAME=VALUE\\0`\r\n/// UTF-16 strings ended by a final `\\0`). Entries whose name matches an\r\n/// override (case-insensitively, as Windows env names are) are replaced;\r\n/// unmatched overrides are appended. Returns a fresh, double-null-terminated\r\n/// block.\r\n///\r\n/// This is the seam that keeps an explicit `SPT_HOME` (the universe\r\n/// relocation knob — `spt_store::perch::spt_home`) alive across the Windows\r\n/// de-elevation respawn. `CreateEnvironmentBlock` rebuilds the desktop\r\n/// user's variables from scratch (to fix `%LOCALAPPDATA%` when UAC elevated\r\n/// through a different admin account), which would otherwise DROP the\r","truncated":true,"truncatedBy":"lines","totalLines":963,"totalBytes":3984,"outputLines":78,"outputBytes":3984,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"//! De-elevation seam for daemon spawn (KNOWN-HAZARDS 5.7).\r\n//!\r\n//! Membership-implies-reachability made every `spt` invocation a potential\r\n//! daemon spawner (`ensure_running`) — including the elevation-gated ones\r\n//! (`subnet create`/`join`, REQ-SUBNET-4). A daemon spawned with the\r\n//! elevated token is poison: on Windows its named pipes deny unelevated\r\n//! clients (every later `spt` reads not-running → spawn → bind\r\n//! Access-denied); on Linux a sudo'd spawn roots the daemon and — because\r\n//! sudo flips `$HOME` — can mint a *different node identity* in root's\r\n//! universe (the key-flip behind the 4.10 ghost rows).\r\n//!\r\n//! The invariant: **the daemon always runs unelevated in the invoking\r\n//! user's universe, regardless of which command spawns it.** Two\r\n//! enforcement points consume this module:\r\n//!\r\n//! - [`crate::daemon::spawn_detached`] de-elevates the child it launches —\r\n//!   Windows: the UAC **linked token** ([`spawn_deelevated`]); Linux: drop\r\n//!   the child to `SUDO_UID`/`SUDO_GID` with the invoker's `HOME`\r\n//!   ([`sudo_invoker`] + `CommandExt::uid/gid`).\r\n//! - The [`crate::daemon::Daemon::run`] entry guard catches a *directly*\r\n//!   elevated `spt daemon` — Linux drops privileges in-process\r\n//!   ([`SudoInvoker::drop_in_process`]) before touching any state; Windows\r\n//!   respawns de-elevated and exits.\r\n//!\r\n//! When no unelevated identity exists to drop to (UAC disabled, genuine\r\n//! root login, SYSTEM), callers run as-is with a loud warning — a\r\n//! consistent universe, never a torn one. Elevated one-shot *clients*\r\n//! talking downward to an unelevated daemon are fine; the daemon side is\r\n//! the invariant.\r\n//!\r\n//! The OS probes mirror `spt`'s REQ-PAIR-6 `elevation.rs` (the CLI gate);\r\n//! they are re-declared here because the dependency arrow points the other\r\n//! way (`spt` → `spt-daemon`). Like [`crate::daemon::detached_no_inherit`],\r\n//! the Windows calls are raw `extern \"system\"` declarations (advapi32 is in\r\n//! the MSVC default link set) — no new dependency for one seam.\r\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\r\n\r\n#[cfg(unix)]\r\npub use unix::{daemon_target_user, sudo_invoker, SudoInvoker};\r\n\r\n#[cfg(windows)]\r\npub use windows::{has_deelevation_target, is_elevated, spawn_deelevated};\r\n\r\n/// What a spawner should do about elevation — the pure decision seam (the\r\n/// REQ-PAIR-6 `gate_for` posture: the OS probe is environment-dependent,\r\n/// the decision is unit-tested both ways).\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub enum SpawnPlan {\r\n    /// Not elevated (or no de-elevation target exists): spawn as-is.\r\n    Inherit,\r\n    /// Elevated with an unelevated identity available: spawn de-elevated.\r\n    DeElevate,\r\n}\r\n\r\n/// The pure decision: de-elevate iff the process is elevated AND an\r\n/// unelevated identity exists to drop to (Windows: the UAC linked token;\r\n/// Linux: a parsed `SUDO_UID`). Elevated-without-target spawns `Inherit`\r\n/// — a consistent elevated universe with a loud warning beats a torn one.\r\n// [impl->REQ-HAZARD-ELEVATED-DAEMON-SPAWN]\r\npub fn plan_spawn(elevated: bool, has_deelevation_target: bool) -> SpawnPlan {\r\n    if elevated && has_deelevation_target {\r\n        SpawnPlan::DeElevate\r\n    } else {\r\n        SpawnPlan::Inherit\r\n    }\r\n}\r\n\r\n/// Overlay `overrides` onto a Windows environment block (`NAME=VALUE\\0`\r\n/// UTF-16 strings ended by a final `\\0`). Entries whose name matches an\r\n/// override (case-insensitively, as Windows env names are) are replaced;\r\n/// unmatched overrides are appended. Returns a fresh, double-null-terminated\r\n/// block.\r\n///\r\n/// This is the seam that keeps an explicit `SPT_HOME` (the universe\r\n/// relocation knob — `spt_store::perch::spt_home`) alive across the Windows\r\n/// de-elevation respawn. `CreateEnvironmentBlock` rebuilds the desktop\r\n/// user's variables from scratch (to fix `%LOCALAPPDATA%` when UAC elevated\r\n/// through a different admin account), which would otherwise DROP the\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]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-daemon\\src\\deelevate.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":963,"totalBytes":3984,"outputLines":78,"outputBytes":3984,"shownRange":{"start":1,"end":78},"nextOffset":79}}},"isError":false,"timestamp":1784197504289}}
{"type":"message","id":"1a124f82","parentId":"2b797f20","timestamp":"2026-07-16T10:25:04.289Z","message":{"role":"toolResult","toolCallId":"call_mjGyPxrBF8rdhOcK5x0ma6ZJ|fc_017bff223290b7ba016a58b1803d24819aa209236a4cba9e55","toolName":"read","content":[{"type":"text","text":"| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the \"no connection\" state in `subnet status` + the coming-online banner | M7 acceptance run (2026-06-06) | User-scope installer is deliberately non-OS-entangled (REQ-INSTALL-2); needs the elevated-install / OS-service design anyway | M8, with the OS-service + Linux-elevation install work |\n| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8) | M7 acceptance run (2026-06-06) | The membership-implies-reachability activation made elevated commands daemon-spawners — correct goal, wrong token | DONE (real elevated-spawn verification on both OSes rides the next acceptance pass) |\n| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruning verb) | DONE (fleet cleanup: 09ef831e ghost rows on both BIGNET nodes decay once the new build deploys) |\n| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\n| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite holding only the elevated children, linked back to the unelevated daemon — rides the REQ-CONSENT-2 escalation framework (allow-once/always) + the reserved-but-refusing capability pattern | Elevated-endpoint design grill (user 2026-06-06) | Real elevation-granting U…\n| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\n| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache seam the pumps consult before the resolver | M8 polish |\n| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\n| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking re…\n| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version fleet safe, old peers skip the new lines). Merged under the same epoch lease, evicted with the node, gated identically to instance feeds. The pump emits one label record per served subnet regardless of endpoint count; `node_status_rows` reads it to NAME an endpoint-l…\n\n[Showing lines 23-32 of 51. Use :33 to continue. Some lines truncated to 768 chars]"}],"details":{"truncation":{"content":"| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the \"no connection\" state in `subnet status` + the coming-online banner | M7 acceptance run (2026-06-06) | User-scope installer is deliberately non-OS-entangled (REQ-INSTALL-2); needs the elevated-install / OS-service design anyway | M8, with the OS-service + Linux-elevation install work |\r\n| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8) | M7 acceptance run (2026-06-06) | The membership-implies-reachability activation made elevated commands daemon-spawners — correct goal, wrong token | DONE (real elevated-spawn verification on both OSes rides the next acceptance pass) |\r\n| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruning verb) | DONE (fleet cleanup: 09ef831e ghost rows on both BIGNET nodes decay once the new build deploys) |\r\n| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\r\n| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite holding only the elevated children, linked back to the unelevated daemon — rides the REQ-CONSENT-2 escalation framework (allow-once/always) + the reserved-but-refusing capability pattern | Elevated-endpoint design grill (user 2026-06-06) | Real elevation-granting U…\n| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\r\n| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache seam the pumps consult before the resolver | M8 polish |\r\n| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\r\n| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking re…\n| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version fleet safe, old peers skip the new lines). Merged under the same epoch lease, evicted with the node, gated identically to instance feeds. The pump emits one label record per served subnet regardless of endpoint count; `node_status_rows` reads it to NAME an endpoint-l…","truncated":true,"truncatedBy":"lines","totalLines":29,"totalBytes":7161,"outputLines":10,"outputBytes":7161,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"| Windows firewall registration for the installed binary: a DETACHED daemon never shows the first-bind firewall prompt, so inbound UDP (mDNS + QUIC meet/pairing) is silently dropped — found as the M7 acceptance NO_SEED_HOLDER (dev/test paths had accumulated allow rules; the install path had none). Options: an elevated install leg adds the rule (`New-NetFirewallRule`), and/or the daemon self-detects blocked inbound and renders it as the \"no connection\" state in `subnet status` + the coming-online banner | M7 acceptance run (2026-06-06) | User-scope installer is deliberately non-OS-entangled (REQ-INSTALL-2); needs the elevated-install / OS-service design anyway | M8, with the OS-service + Linux-elevation install work |\r\n| ~~**Non-admin daemon spawn (both OSes)**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 5.7, REQ-HAZARD-ELEVATED-DAEMON-SPAWN): de-elevated spawn at `spawn_detached` (Windows: UAC linked token via `CreateProcessWithTokenW`; Linux: drop child to SUDO_UID/GID) + `Daemon::run` entry guard + the unix `spt` main-entry sudo drop (whole process re-anchored to the invoker's universe, elevation kept PROVEN for the gated commands). Remaining Linux follow-up = the install-symlink + default-account election row below (M8) | M7 acceptance run (2026-06-06) | The membership-implies-reachability activation made elevated commands daemon-spawners — correct goal, wrong token | DONE (real elevated-spawn verification on both OSes rides the next acceptance pass) |\r\n| ~~**Ghost registry row eviction**~~ **SHIPPED 2026-06-06** (KNOWN-HAZARDS 4.10, REQ-HAZARD-REGISTRY-GHOST-ROWS): silent-peer decay — rows from a node unheard for `registry_evict_after_ms` (default 300s ≈ 10 pump cadences) evict on the registry pump tick, snapshots rewritten; own rows never decay. STALE trust rows for a dead identity still need a manual prune verb (below) | M7 acceptance run (2026-06-06) | Needs a decay/eviction policy (e.g. drop rows unrefreshed for N pump cadences; trust-row pruning verb) | DONE (fleet cleanup: 09ef831e ghost rows on both BIGNET nodes decay once the new build deploys) |\r\n| Trust-row prune verb (`spt subnet prune <node>` or similar): registry GHOST rows now decay (4.10), but the dead identity's TRUST rows persist by design (trust is a user decision) — they cost dead dials every pump tick and clutter status views. BIGNET carries two such rows (09ef831e, both sides) | Ghost-eviction fix (2026-06-06) | Trust mutation = security surface; wants the elevation gate + the M8 noun shapes | M8 CLI nounification |\r\n| Elevated spt-hosted endpoints — a consented elevation satellite: the broker lives inside the always-unelevated daemon (KH 5.7), so spt-hosted children are unelevated by construction; there is NO silent unelevated→elevated path on either OS (that is UAC/sudo's whole point). An agent needing admin powers is harness-hosted-elevated today (works: TCP listeners + talk-down pipes). The spt-hosted form needs a per-grant CONSENTED elevation: UAC prompt / polkit spawns a small elevated PTY-host satellite holding only the elevated children, linked back to the unelevated daemon — rides the REQ-CONSENT-2 escalation framework (allow-once/always) + the reserved-but-refusing capability pattern | Elevated-endpoint design grill (user 2026-06-06) | Real elevation-granting U…\n| ~~AMBIGUOUS render~~ **SHIPPED 2026-06-06** (rode the ghost-eviction fix): resolution refusals render valid copy-paste targets at the wansend boundary (`render_refusal`) — labels preferred, key-prefix on label collision, `subnet:id` for cross-subnet | M7 acceptance run (user 2026-06-06) | Pure render fix at the wansend/CLI boundary; carries into the M8 nounification sweep | DONE |\r\n| Post-join address seeding: the pairing ceremony holds a live, authenticated connection to the peer, but the peer pumps then re-find each other from scratch via id-only discovery (fresh node ids must propagate to n0 DNS first) — observed live as ~1 min to first `status --nodes` convergence after a join. Seed the pump's address cache from the ceremony connection (both sides) for instant first sync | M7 acceptance run (2026-06-06) | Convergence is correct, just slow on first contact; needs an addr-cache seam the pumps consult before the resolver | M8 polish |\r\n| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |\r\n| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill): `spt endpoint` namespace absorbs fork/suspend/wake/shutdown/rename/stop/digest + `access` (per-endpoint store — NOT subnet-scoped) + `description` (ex-`resources` blurb; bare = show, `set` = author); `spt endpoint list [--local\\|--subnet <name>]` = merged listing, default all-subnets grouped by subnet, SELF endpoint pinned distinctly at top (bare `spt endpoint` = this list); agent HOT PATH stays flat (`send`/`ring`/`ready`/`whoami`/`how-to`); `notify` → `spt subnet notify`; `notif` stays top-level (user-recall, cross-subnet); `spt daemon stop\\|status` noun (hidden `daemon` → `daemon run`); agent-endpoint `shutdown` keeps its name | M7 grill (2026-06-06) | Second surface-breaking re…\n| ~~Node label for an endpoint-less node (REQ-SUBNET-3 gap)~~ **SHIPPED 2026-06-08** (option 2 — node-level presence datum): a peer with **zero endpoints** rendered as a bare key-prefix in `status --nodes` because `node_label` only rode endpoint `Instance` rows. FIX: a node-LEVEL carrier — `SubnetRegistry.node_labels` (node→label) + a `NodeLabelUpdate` feed record riding the existing registry replication stream as an untagged `RegistryFeedRecord` variant (instance bytes unchanged → mixed-version fleet safe, old peers skip the new lines). Merged under the same epoch lease, evicted with the node, gated identically to instance feeds. The pump emits one label record per served subnet regardless of endpoint count; `node_status_rows` reads it to NAME an endpoint-l…","startLine":23,"lineNumbers":[23,24,25,26,27,28,29,30,31,32]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\docs\\DEFERRED.md"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":51,"totalBytes":7161,"outputLines":10,"outputBytes":7161,"shownRange":{"start":23,"end":32},"nextOffset":33},"limits":{"columnTruncated":{"maxColumn":768}}}},"isError":false,"timestamp":1784197504289}}
{"type":"message","id":"99ce2b8d","parentId":"1a124f82","timestamp":"2026-07-16T10:25:16.955Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting endpoint messaging constraints**"},{"type":"toolCall","id":"call_9kfoGeuSMnDaXfsdzD9O5CnR|fc_017bff223290b7ba016a58b18cd768819a8af3bdeee452fd6b","name":"grep","arguments":{"i":"Finding field report references","pattern":"hertz|elevated.*message|message.*elevated|elevated.*rc|rc.*elevated","path":"docs; .planning; README.md; CHANGELOG.md","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_Wlw5EiwnwBxjEdnmQSWjFlg6|fc_017bff223290b7ba016a58b18cd77c819a89df0976e3df917a","name":"grep","arguments":{"i":"Finding topology test coverage","pattern":"harness_only|controllable.*false|No live session|harness-hosted|api listen","path":"crates/spt/tests; crates/spt-daemon/tests; crates/spt/src/rc.rs; crates/spt-daemon/src/inject.rs","selector":null,"case":false,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_eUvUDlrPWlgSkvPZNtXQkch5|fc_017bff223290b7ba016a58b18cd7b8819a90635adaae1f9d33","name":"read","arguments":{"i":"Reading send command definition","path":"crates/spt/src/cli.rs","selector":"55-135"}},{"type":"toolCall","id":"call_flWsju2zapi6ICfW4ln2cpsl|fc_017bff223290b7ba016a58b18cd7c8819aa196f8cbf7ef7e68","name":"read","arguments":{"i":"Reading address parsing","path":"crates/spt-proto/src/addr.rs","selector":"1-200"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":7135,"output":366,"cacheRead":202240,"cacheWrite":0,"totalTokens":209741,"reasoningTokens":110,"cost":{"input":0.035675000000000005,"output":0.01098,"cacheRead":0.10112,"cacheWrite":0,"total":0.147775}},"stopReason":"toolUse","timestamp":1784197504301,"responseId":"resp_017bff223290b7ba016a58b180a82c819abef959679807e0f8","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b1860c3c819a9161a2c09c5810b0","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLGMUocJsZxs3cIXX_JDQnzQh-NwHkOis05sDoQPDS2UKx2H5qw8E20JmOki1sBtCmUnFr6K6t6Jm1kL0FJ7AhhJ2Z2nROR4jkp1Lvzzc8t8OU4au-1Dxy0HQU1lU8EBE7K6_B9zejs8hgjWh1pM6Eq3Dtaiz9PBml2bp_5jmf0_vChPSabTwOB0Hm_QqzYpD1vd3iqmwh31wB-qI3hNioxrRp-iZI7hmX6LbifxYy8irgz7n7kHkAO-SSeMb1LARDjtzQDX4TrXRJM4JyTMtIZ8GI6wMXW1H1zU6n8uOuPo_Du9nAIbJLbvCBtvXgr-YufqhvgmjNG84a7S5oobEp9w8EcfcBErOhZNQ8evq1u2EFIRhpsIUBSLaLdCn2vAaJ6EfaI_kndrhhcyEPbVI6jTZ_yaZkb9ADSA_qTELRRvsgC7fi9FI9X_egEsT-ElVCgiDkOv-7J82AqKh2_MW3p4aziTzAqj4P6laMQl0Ovz1HiqrYtaDM8NYNcYUV50He3naFtaUztiNdl9HWF5Uysdb4oEP1ZVKiphY0Nx3n7XZSWix6u9IHFdIii-QTMDZHL8WlHcFjsCI-JICQP6dqqCK5yZGJoA2zul-uhmctsWCYbOtKlFO-tejRFbgRXNfkJh0sBG5R96fKzm1V0NmUqTkIUrkUP1rrCijSnBsnEuK_L0gPpQ-1m5GsjRjT6Ot0ZETDfCoPuXxq7eHaS9kPVAHPLiQvGk-Qw16cEv36R9RJ2eukrDpr3eonrlYQXac4pLhDoXPfh3uYwNxilvyyiprb55b1z94m-blBZJOV9-nmOGfc0D3MDdkNL6J0w6CLTL8OmDBVBPRTDSXCW77sQcsp6k4-f4jh3-_adHXEdSsl0dIeHP2mTH_1oTo1r5YBfCo15L6N3iic9HmAe2X6B1brEhlS5ucvHComt-f478woi5UvZ8WgOUSPq4EMlNqmn_TkABSiBN5dwQAD9rt40Dg-OkPgrSjB4ZMtbegbv4nkOEhNL0FLFbdM3d_AD7HRiN_ZXemBBCkif_HNNmxfP8NFASlmgjgQX2K_j-eoPLV_nPeC8t_9uG8N5Lx9-oeVasETQ4P3bGe_yKb8yZ1hOtmpmEU-qhX3dy2olYeP1QKEo6D6fXbimO2z0NnKT8Drm7lg5TqWaRztAarpKW20vM3ihV7G09pEqvHj4YCpwmkYa7UGmCyQpHQVdUzX5ikBkugKElKXuZf_ibi8xXHGV6cylvdmbunswXOzKTpVbnn80yQ8SBtV9faGYCNOpq0Q4q7IwkwMAhLUeUZUM0rnmTg6xKL-tJKjxf4HCGayKTPq8tr17Ds4Ndu1it2t08y94ljyBlXM9PHJoKznne1iyi4OlPSyFzTD_jjWoTbueELKc8-_Zwy3sR3Uxi6Jqt1T2urcPjIVEPikD7-t3lgXWQvw2dgk-B0NStclduvF-ncGPj6IqZiZlLqP5QJV4O7riGCU4Nr6ySzoNJYHEK_maIrVqSu8k8iC_vLe4sxEzoE046uh22Tfmna3Pw2YS6u1m4UpP9mrljmHM9F7yAp5zJKnhTyz86JyD7TPPe7RVhlAfdwk5XgKrV3Rz3fRzrjiPIqKt8vThJf36zacUgzK4pDpxFv2UfS97MEU9W-Xwtv1MDnSqkCmKlj8c773gsrS2nMhj4HSz2KtylqBcaTvQBdQ==","summary":[{"type":"summary_text","text":"**Inspecting endpoint messaging constraints**"}]},{"id":"fc_017bff223290b7ba016a58b18cd768819a8af3bdeee452fd6b","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding field report references\",\"pattern\":\"hertz|elevated.*message|message.*elevated|elevated.*rc|rc.*elevated\",\"path\":\"docs; .planning; README.md; CHANGELOG.md\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_9kfoGeuSMnDaXfsdzD9O5CnR","name":"grep"},{"id":"fc_017bff223290b7ba016a58b18cd77c819a89df0976e3df917a","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding topology test coverage\",\"pattern\":\"harness_only|controllable.*false|No live session|harness-hosted|api listen\",\"path\":\"crates/spt/tests; crates/spt-daemon/tests; crates/spt/src/rc.rs; crates/spt-daemon/src/inject.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_Wlw5EiwnwBxjEdnmQSWjFlg6","name":"grep"},{"id":"fc_017bff223290b7ba016a58b18cd7b8819a90635adaae1f9d33","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading send command definition\",\"path\":\"crates/spt/src/cli.rs\",\"selector\":\"55-135\"}","call_id":"call_eUvUDlrPWlgSkvPZNtXQkch5","name":"read"},{"id":"fc_017bff223290b7ba016a58b18cd7c8819aa196f8cbf7ef7e68","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading address parsing\",\"path\":\"crates/spt-proto/src/addr.rs\",\"selector\":\"1-200\"}","call_id":"call_flWsju2zapi6ICfW4ln2cpsl","name":"read"}]},"duration":12645.7583999997,"ttft":5719.0202000001445,"contextSnapshot":{"promptTokens":209375,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9kfoGeuSMnDaXfsdzD9O5CnR|fc_017bff223290b7ba016a58b18cd768819a8af3bdeee452fd6b","toolName":"grep","startedAt":"2026-07-16T10:25:16.955Z","args":{"path":"docs; .planning; README.md; CHANGELOG.md"},"intent":"Finding field report references"},"id":"f54bef07","parentId":"99ce2b8d","timestamp":"2026-07-16T10:25:16.955Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_Wlw5EiwnwBxjEdnmQSWjFlg6|fc_017bff223290b7ba016a58b18cd77c819a89df0976e3df917a","toolName":"grep","startedAt":"2026-07-16T10:25:16.955Z","args":{"path":"crates/spt/tests; crates/spt-daemon/tests; crates/spt/src/rc.rs; crates/spt-daemon/src/inject.rs"},"intent":"Finding topology test coverage"},"id":"1405b088","parentId":"f54bef07","timestamp":"2026-07-16T10:25:16.955Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_eUvUDlrPWlgSkvPZNtXQkch5|fc_017bff223290b7ba016a58b18cd7b8819a90635adaae1f9d33","toolName":"read","startedAt":"2026-07-16T10:25:16.955Z","args":{"path":"crates/spt/src/cli.rs"},"intent":"Reading send command definition"},"id":"67d924f7","parentId":"1405b088","timestamp":"2026-07-16T10:25:16.955Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_flWsju2zapi6ICfW4ln2cpsl|fc_017bff223290b7ba016a58b18cd7c8819aa196f8cbf7ef7e68","toolName":"read","startedAt":"2026-07-16T10:25:16.955Z","args":{"path":"crates/spt-proto/src/addr.rs"},"intent":"Reading address parsing"},"id":"738e2633","parentId":"67d924f7","timestamp":"2026-07-16T10:25:16.955Z"}
{"type":"message","id":"d0483992","parentId":"738e2633","timestamp":"2026-07-16T10:25:16.957Z","message":{"role":"toolResult","toolCallId":"call_flWsju2zapi6ICfW4ln2cpsl|fc_017bff223290b7ba016a58b18cd7c8819aa196f8cbf7ef7e68","toolName":"read","content":[{"type":"text","text":"//! Qualified-address grammar `[subnet:]id[@node]` (D3c, ADR-0006 §2 / REQ-INST-10).\n//!\n//! The canonical way to name an endpoint across the multi-subnet, multi-node fabric.\n//! Two optional qualifiers combine around the bare endpoint id:\n//!   - a **subnet** qualifier `home:ling` selects which subnet's registry to resolve in;\n//!   - a **node** qualifier `ling@hfenduleam` pins a specific running instance.\n//!\n//! Both delimiters (`:`, `@`) are permanently reserved from the id charset\n//! ([`crate::id`], REQ-HAZARD-ID-CHARSET), so parsing is unambiguous: there is at most\n//! one `:` (before the id) and at most one `@` (after the id), and every component is a\n//! valid addressable name in its own right. This module is **grammar only** — picking an\n//! instance from a registry is the resolution policy (`spt-net::net::registry`).\n// [impl->REQ-INST-10]\n\nuse crate::id::{validate_endpoint_id, IdError};\n\n/// Which component of a qualified address failed validation.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum Part {\n    /// The `subnet:` qualifier.\n    Subnet,\n    /// The bare endpoint id.\n    Id,\n    /// The `@node` qualifier.\n    Node,\n}\n\n/// Why a qualified address failed to parse: a named component violated the id charset\n/// contract (empty, too long, or a disallowed character).\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct AddrError {\n    /// The offending component.\n    pub part: Part,\n    /// The underlying charset/length violation.\n    pub source: IdError,\n}\n\nimpl std::fmt::Display for AddrError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let part = match self.part {\n            Part::Subnet => \"subnet qualifier\",\n            Part::Id => \"endpoint id\",\n            Part::Node => \"node qualifier\",\n        };\n        write!(f, \"invalid {part}: {}\", self.source)\n    }\n}\n\nimpl std::error::Error for AddrError {}\n\n/// A parsed qualified address. `subnet`/`node` are present only when their qualifier was\n/// given; `id` is always present and valid.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct Address {\n    /// The optional subnet qualifier (before `:`).\n    pub subnet: Option<String>,\n    /// The bare endpoint id.\n    pub id: String,\n    /// The optional node qualifier (after `@`).\n    pub node: Option<String>,\n}\n\nimpl Address {\n    /// Parse `[subnet:]id[@node]`. Every present component is validated against the id\n    /// charset; the reserved delimiters guarantee an unambiguous split. A second `:` or\n    /// `@` lands inside a component and is rejected as a disallowed character there.\n    // [impl->REQ-INST-10]\n    pub fn parse(s: &str) -> Result<Address, AddrError> {\n        // Split the node qualifier off the end (`@` is after the id). `rsplit` so a\n        // stray earlier `@` stays in `head` and is caught when `head`/id is validated.\n        let (head, node) = match s.rfind('@') {\n            Some(i) => (&s[..i], Some(&s[i + 1..])),\n            None => (s, None),\n        };\n        // Split the subnet qualifier off the front (`:` is before the id). First `:` —\n        // a second one lands in the id and is rejected by the charset validator.\n        let (subnet, id) = match head.find(':') {\n            Some(i) => (Some(&head[..i]), &head[i + 1..]),\n            None => (None, head),\n        };\n\n        validate_endpoint_id(id).map_err(|source| AddrError {\n            part: Part::Id,\n            source,\n        })?;\n        if let Some(sub) = subnet {\n            validate_endpoint_id(sub).map_err(|source| AddrError {\n                part: Part::Subnet,\n                source,\n            })?;\n        }\n        if let Some(n) = node {\n            validate_endpoint_id(n).map_err(|source| AddrError {\n                part: Part::Node,\n                source,\n            })?;\n        }\n\n        Ok(Address {\n            subnet: subnet.map(str::to_string),\n            id: id.to_string(),\n            node: node.map(str::to_string),\n        })\n    }\n\n    /// A bare, unqualified address (just an id) — convenience for the common case.\n    pub fn bare(id: &str) -> Result<Address, AddrError> {\n        Address::parse(id)\n    }\n}\n\n/// Render back to the canonical `[subnet:]id[@node]` string.\nimpl std::fmt::Display for Address {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        if let Some(sub) = &self.subnet {\n            write!(f, \"{sub}:\")?;\n        }\n        write!(f, \"{}\", self.id)?;\n        if let Some(node) = &self.node {\n            write!(f, \"@{node}\")?;\n        }\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // [unit->REQ-INST-10] all four shapes parse into the right components.\n    #[test]\n    fn parses_every_qualifier_combination() {\n        assert_eq!(\n            Address::parse(\"ling\").unwrap(),\n            Address {\n                subnet: None,\n                id: \"ling\".into(),\n                node: None\n            }\n        );\n        assert_eq!(\n            Address::parse(\"home:ling\").unwrap(),\n            Address {\n                subnet: Some(\"home\".into()),\n                id: \"ling\".into(),\n                node: None\n            }\n        );\n        assert_eq!(\n            Address::parse(\"ling@hfenduleam\").unwrap(),\n            Address {\n                subnet: None,\n                id: \"ling\".into(),\n                node: Some(\"hfenduleam\".into())\n            }\n        );\n        assert_eq!(\n            Address::parse(\"home:ling@hfenduleam\").unwrap(),\n            Address {\n                subnet: Some(\"home\".into()),\n                id: \"ling\".into(),\n                node: Some(\"hfenduleam\".into())\n            }\n        );\n    }\n\n    // [unit->REQ-INST-10] Display round-trips the canonical form.\n    #[test]\n    fn display_round_trips() {\n        for s in [\"ling\", \"home:ling\", \"ling@node\", \"home:ling@node\"] {\n            assert_eq!(Address::parse(s).unwrap().to_string(), s);\n        }\n    }\n\n    // [unit->REQ-INST-10] a second delimiter lands inside a component and is rejected\n    // (no silent mis-split) — the reserved-charset guarantee in action.\n    #[test]\n    fn rejects_doubled_delimiters() {\n        assert_eq!(Address::parse(\"a:b:c\").unwrap_err().part, Part::Id);\n        assert_eq!(Address::parse(\"a@b@c\").unwrap_err().part, Part::Id);\n    }\n\n    // [unit->REQ-INST-10] each component is charset-validated; empties are rejected.\n    #[test]\n    fn rejects_empty_or_bad_components() {\n        assert_eq!(Address::parse(\"home:\").unwrap_err().part, Part::Id);\n        assert_eq!(Address::parse(\":ling\").unwrap_err().part, Part::Subnet);\n        assert_eq!(Address::parse(\"ling@\").unwrap_err().part, Part::Node);\n        assert_eq!(Address::parse(\"\").unwrap_err().part, Part::Id);\n    }\n}\n"}],"details":{"displayContent":{"text":"//! Qualified-address grammar `[subnet:]id[@node]` (D3c, ADR-0006 §2 / REQ-INST-10).\r\n//!\r\n//! The canonical way to name an endpoint across the multi-subnet, multi-node fabric.\r\n//! Two optional qualifiers combine around the bare endpoint id:\r\n//!   - a **subnet** qualifier `home:ling` selects which subnet's registry to resolve in;\r\n//!   - a **node** qualifier `ling@hfenduleam` pins a specific running instance.\r\n//!\r\n//! Both delimiters (`:`, `@`) are permanently reserved from the id charset\r\n//! ([`crate::id`], REQ-HAZARD-ID-CHARSET), so parsing is unambiguous: there is at most\r\n//! one `:` (before the id) and at most one `@` (after the id), and every component is a\r\n//! valid addressable name in its own right. This module is **grammar only** — picking an\r\n//! instance from a registry is the resolution policy (`spt-net::net::registry`).\r\n// [impl->REQ-INST-10]\r\n\r\nuse crate::id::{validate_endpoint_id, IdError};\r\n\r\n/// Which component of a qualified address failed validation.\r\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\r\npub enum Part {\r\n    /// The `subnet:` qualifier.\r\n    Subnet,\r\n    /// The bare endpoint id.\r\n    Id,\r\n    /// The `@node` qualifier.\r\n    Node,\r\n}\r\n\r\n/// Why a qualified address failed to parse: a named component violated the id charset\r\n/// contract (empty, too long, or a disallowed character).\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub struct AddrError {\r\n    /// The offending component.\r\n    pub part: Part,\r\n    /// The underlying charset/length violation.\r\n    pub source: IdError,\r\n}\r\n\r\nimpl std::fmt::Display for AddrError {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        let part = match self.part {\r\n            Part::Subnet => \"subnet qualifier\",\r\n            Part::Id => \"endpoint id\",\r\n            Part::Node => \"node qualifier\",\r\n        };\r\n        write!(f, \"invalid {part}: {}\", self.source)\r\n    }\r\n}\r\n\r\nimpl std::error::Error for AddrError {}\r\n\r\n/// A parsed qualified address. `subnet`/`node` are present only when their qualifier was\r\n/// given; `id` is always present and valid.\r\n#[derive(Debug, Clone, PartialEq, Eq)]\r\npub struct Address {\r\n    /// The optional subnet qualifier (before `:`).\r\n    pub subnet: Option<String>,\r\n    /// The bare endpoint id.\r\n    pub id: String,\r\n    /// The optional node qualifier (after `@`).\r\n    pub node: Option<String>,\r\n}\r\n\r\nimpl Address {\r\n    /// Parse `[subnet:]id[@node]`. Every present component is validated against the id\r\n    /// charset; the reserved delimiters guarantee an unambiguous split. A second `:` or\r\n    /// `@` lands inside a component and is rejected as a disallowed character there.\r\n    // [impl->REQ-INST-10]\r\n    pub fn parse(s: &str) -> Result<Address, AddrError> {\r\n        // Split the node qualifier off the end (`@` is after the id). `rsplit` so a\r\n        // stray earlier `@` stays in `head` and is caught when `head`/id is validated.\r\n        let (head, node) = match s.rfind('@') {\r\n            Some(i) => (&s[..i], Some(&s[i + 1..])),\r\n            None => (s, None),\r\n        };\r\n        // Split the subnet qualifier off the front (`:` is before the id). First `:` —\r\n        // a second one lands in the id and is rejected by the charset validator.\r\n        let (subnet, id) = match head.find(':') {\r\n            Some(i) => (Some(&head[..i]), &head[i + 1..]),\r\n            None => (None, head),\r\n        };\r\n\r\n        validate_endpoint_id(id).map_err(|source| AddrError {\r\n            part: Part::Id,\r\n            source,\r\n        })?;\r\n        if let Some(sub) = subnet {\r\n            validate_endpoint_id(sub).map_err(|source| AddrError {\r\n                part: Part::Subnet,\r\n                source,\r\n            })?;\r\n        }\r\n        if let Some(n) = node {\r\n            validate_endpoint_id(n).map_err(|source| AddrError {\r\n                part: Part::Node,\r\n                source,\r\n            })?;\r\n        }\r\n\r\n        Ok(Address {\r\n            subnet: subnet.map(str::to_string),\r\n            id: id.to_string(),\r\n            node: node.map(str::to_string),\r\n        })\r\n    }\r\n\r\n    /// A bare, unqualified address (just an id) — convenience for the common case.\r\n    pub fn bare(id: &str) -> Result<Address, AddrError> {\r\n        Address::parse(id)\r\n    }\r\n}\r\n\r\n/// Render back to the canonical `[subnet:]id[@node]` string.\r\nimpl std::fmt::Display for Address {\r\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\r\n        if let Some(sub) = &self.subnet {\r\n            write!(f, \"{sub}:\")?;\r\n        }\r\n        write!(f, \"{}\", self.id)?;\r\n        if let Some(node) = &self.node {\r\n            write!(f, \"@{node}\")?;\r\n        }\r\n        Ok(())\r\n    }\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n\r\n    // [unit->REQ-INST-10] all four shapes parse into the right components.\r\n    #[test]\r\n    fn parses_every_qualifier_combination() {\r\n        assert_eq!(\r\n            Address::parse(\"ling\").unwrap(),\r\n            Address {\r\n                subnet: None,\r\n                id: \"ling\".into(),\r\n                node: None\r\n            }\r\n        );\r\n        assert_eq!(\r\n            Address::parse(\"home:ling\").unwrap(),\r\n            Address {\r\n                subnet: Some(\"home\".into()),\r\n                id: \"ling\".into(),\r\n                node: None\r\n            }\r\n        );\r\n        assert_eq!(\r\n            Address::parse(\"ling@hfenduleam\").unwrap(),\r\n            Address {\r\n                subnet: None,\r\n                id: \"ling\".into(),\r\n                node: Some(\"hfenduleam\".into())\r\n            }\r\n        );\r\n        assert_eq!(\r\n            Address::parse(\"home:ling@hfenduleam\").unwrap(),\r\n            Address {\r\n                subnet: Some(\"home\".into()),\r\n                id: \"ling\".into(),\r\n                node: Some(\"hfenduleam\".into())\r\n            }\r\n        );\r\n    }\r\n\r\n    // [unit->REQ-INST-10] Display round-trips the canonical form.\r\n    #[test]\r\n    fn display_round_trips() {\r\n        for s in [\"ling\", \"home:ling\", \"ling@node\", \"home:ling@node\"] {\r\n            assert_eq!(Address::parse(s).unwrap().to_string(), s);\r\n        }\r\n    }\r\n\r\n    // [unit->REQ-INST-10] a second delimiter lands inside a component and is rejected\r\n    // (no silent mis-split) — the reserved-charset guarantee in action.\r\n    #[test]\r\n    fn rejects_doubled_delimiters() {\r\n        assert_eq!(Address::parse(\"a:b:c\").unwrap_err().part, Part::Id);\r\n        assert_eq!(Address::parse(\"a@b@c\").unwrap_err().part, Part::Id);\r\n    }\r\n\r\n    // [unit->REQ-INST-10] each component is charset-validated; empties are rejected.\r\n    #[test]\r\n    fn rejects_empty_or_bad_components() {\r\n        assert_eq!(Address::parse(\"home:\").unwrap_err().part, Part::Id);\r\n        assert_eq!(Address::parse(\":ling\").unwrap_err().part, Part::Subnet);\r\n        assert_eq!(Address::parse(\"ling@\").unwrap_err().part, Part::Node);\r\n        assert_eq!(Address::parse(\"\").unwrap_err().part, Part::Id);\r\n    }\r\n}\r\n","startLine":1,"lineNumbers":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-proto\\src\\addr.rs"}}},"isError":false,"timestamp":1784197516957}}
{"type":"message","id":"27cb5c83","parentId":"d0483992","timestamp":"2026-07-16T10:25:17.077Z","message":{"role":"toolResult","toolCallId":"call_eUvUDlrPWlgSkvPZNtXQkch5|fc_017bff223290b7ba016a58b18cd7b8819a90635adaae1f9d33","toolName":"read","content":[{"type":"text","text":"struct Cli {\n    /// Emit machine-readable JSON instead of the human view. Honored by the\n    /// read/status commands (list, whoami, status, description, role, the *-list\n    /// queries, how-to); action commands ignore it.\n    // [impl->REQ-CLI-JSON]\n    #[arg(long, global = true)]\n    json: bool,\n    // Optional so a BARE `spt` (no subcommand) parses to `None` instead of a\n    // clap \"required subcommand\" error — bare `spt` opens the endpoint-run\n    // picker on an interactive terminal (REQ-RUN-PICKER). `--help`/`-h`/`help`\n    // are handled by clap before we ever see `None`.\n    #[command(subcommand)]\n    cmd: Option<Cmd>,\n}\n\n#[derive(Subcommand)]\nenum Cmd {\n    /// Send a message (body read from stdin); fire-and-forget.\n    Send {\n        // [impl->REQ-SEND-REPLYTO-REMOVE] `--reply-to` is hard-removed; `target` is\n        // a required positional (no reply-to fallback). Reply-correlation rides the\n        // structural `from`, not a flag.\n        /// Target perch id.\n        target: String,\n        /// Sender id carried structurally as the message `from` (auto-detected from session if omitted).\n        #[arg(long)]\n        from: Option<String>,\n        /// Deliver only when the target is idle (the idle/wake window); hold\n        /// until then and never surface to the target's active poll.\n        #[arg(long, conflicts_with = \"active_only\")]\n        idle_only: bool,\n        /// Deliver only through the target's own poll (the no-interrupt hook\n        /// channel); never wakes an idle target. Replaces the old --deferred.\n        // `alias = \"deferred\"` is a HIDDEN back-compat alias: `--deferred` shipped\n        // as a documented flag in published reference.md, so `spt send --deferred`\n        // keeps parsing (→ active_only) for external scripts/adapters. Hidden (not\n        // `visible_alias`) → it does NOT render in the generated reference.md, so\n        // there is zero docs-drift. (ADR-0028 W2 pure-rename + back-compat.)\n        #[arg(long, alias = \"deferred\")]\n        active_only: bool,\n        /// Drop the message if it cannot be delivered in its window, instead of\n        /// spooling until delivered.\n        #[arg(long)]\n        ephemeral: bool,\n        /// Deliver through the target's translation binary when one is running,\n        /// else fall back to the normal channel. Delivers regardless of idle/active.\n        #[arg(long, conflicts_with = \"force_native\")]\n        prefer_native: bool,\n        /// Deliver ONLY through the target's translation binary — no fallback and\n        /// no spooling. If no binary is running the send is reported undelivered.\n        #[arg(long)]\n        force_native: bool,\n        /// Attach an opaque JSON metadata blob alongside the message body, carried\n        /// verbatim for the receiving adapter to parse. Does not replace the body.\n        #[arg(long, value_name = \"JSON\")]\n        json_payload: Option<String>,\n        /// Request the `user-msg` type (the user's authority). Honored only from\n        /// a user-backed origin (a Gateway endpoint, or the local user's own\n        /// CLI); an agent-family sender is re-stamped to plain `msg`.\n        #[arg(long = \"user-msg\")]\n        user_msg: bool,\n    },\n    /// Send and block for a reply (body read from stdin).\n    ///\n    /// The reply body is printed to stdout; gives up after --timeout seconds.\n    Ring {\n        /// Target perch id.\n        target: String,\n        /// Sender id (auto-detected from session if omitted).\n        #[arg(long)]\n        from: Option<String>,\n        /// Seconds to wait for a reply before giving up.\n        #[arg(long, default_value = \"60\")]\n        timeout: u64,\n    },\n    /// Become reachable: register the perch and listen (blocks).\n    ///\n    /// Drains the spooled backlog first; each received message prints to\n    /// stdout. With --once, runs a single drain+receive cycle and exits.\n    Ready {\n        /// This agent's perch id.\n        id: String,\n        /// Run a single drain+receive cycle, then exit (one-shot fallback\n        /// for harnesses that cannot host a long-running listener).\n        #[arg(long)]\n        once: bool,\n…\n    },\n…\n}\n\n[Showing lines 54-138 of 18900. Use :139 to continue]"}],"details":{"truncation":{"content":"    /// Emit machine-readable JSON instead of the human view. Honored by the\r\n    /// read/status commands (list, whoami, status, description, role, the *-list\r\n    /// queries, how-to); action commands ignore it.\r\n    // [impl->REQ-CLI-JSON]\r\n    #[arg(long, global = true)]\r\n    json: bool,\r\n    // Optional so a BARE `spt` (no subcommand) parses to `None` instead of a\r\n    // clap \"required subcommand\" error — bare `spt` opens the endpoint-run\r\n    // picker on an interactive terminal (REQ-RUN-PICKER). `--help`/`-h`/`help`\r\n    // are handled by clap before we ever see `None`.\r\n    #[command(subcommand)]\r\n    cmd: Option<Cmd>,\r\n}\r\n\r\n#[derive(Subcommand)]\r\nenum Cmd {\r\n    /// Send a message (body read from stdin); fire-and-forget.\r\n    Send {\r\n        // [impl->REQ-SEND-REPLYTO-REMOVE] `--reply-to` is hard-removed; `target` is\r\n        // a required positional (no reply-to fallback). Reply-correlation rides the\r\n        // structural `from`, not a flag.\r\n        /// Target perch id.\r\n        target: String,\r\n        /// Sender id carried structurally as the message `from` (auto-detected from session if omitted).\r\n        #[arg(long)]\r\n        from: Option<String>,\r\n        /// Deliver only when the target is idle (the idle/wake window); hold\r\n        /// until then and never surface to the target's active poll.\r\n        #[arg(long, conflicts_with = \"active_only\")]\r\n        idle_only: bool,\r\n        /// Deliver only through the target's own poll (the no-interrupt hook\r\n        /// channel); never wakes an idle target. Replaces the old --deferred.\r\n        // `alias = \"deferred\"` is a HIDDEN back-compat alias: `--deferred` shipped\r\n        // as a documented flag in published reference.md, so `spt send --deferred`\r\n        // keeps parsing (→ active_only) for external scripts/adapters. Hidden (not\r\n        // `visible_alias`) → it does NOT render in the generated reference.md, so\r\n        // there is zero docs-drift. (ADR-0028 W2 pure-rename + back-compat.)\r\n        #[arg(long, alias = \"deferred\")]\r\n        active_only: bool,\r\n        /// Drop the message if it cannot be delivered in its window, instead of\r\n        /// spooling until delivered.\r\n        #[arg(long)]\r\n        ephemeral: bool,\r\n        /// Deliver through the target's translation binary when one is running,\r\n        /// else fall back to the normal channel. Delivers regardless of idle/active.\r\n        #[arg(long, conflicts_with = \"force_native\")]\r\n        prefer_native: bool,\r\n        /// Deliver ONLY through the target's translation binary — no fallback and\r\n        /// no spooling. If no binary is running the send is reported undelivered.\r\n        #[arg(long)]\r\n        force_native: bool,\r\n        /// Attach an opaque JSON metadata blob alongside the message body, carried\r\n        /// verbatim for the receiving adapter to parse. Does not replace the body.\r\n        #[arg(long, value_name = \"JSON\")]\r\n        json_payload: Option<String>,\r\n        /// Request the `user-msg` type (the user's authority). Honored only from\r\n        /// a user-backed origin (a Gateway endpoint, or the local user's own\r\n        /// CLI); an agent-family sender is re-stamped to plain `msg`.\r\n        #[arg(long = \"user-msg\")]\r\n        user_msg: bool,\r\n    },\r\n    /// Send and block for a reply (body read from stdin).\r\n    ///\r\n    /// The reply body is printed to stdout; gives up after --timeout seconds.\r\n    Ring {\r\n        /// Target perch id.\r\n        target: String,\r\n        /// Sender id (auto-detected from session if omitted).\r\n        #[arg(long)]\r\n        from: Option<String>,\r\n        /// Seconds to wait for a reply before giving up.\r\n        #[arg(long, default_value = \"60\")]\r\n        timeout: u64,\r\n    },\r\n    /// Become reachable: register the perch and listen (blocks).\r\n    ///\r\n    /// Drains the spooled backlog first; each received message prints to\r\n    /// stdout. With --once, runs a single drain+receive cycle and exits.\r\n    Ready {\r\n        /// This agent's perch id.\r\n        id: String,\r\n        /// Run a single drain+receive cycle, then exit (one-shot fallback\r\n        /// for harnesses that cannot host a long-running listener).\r\n        #[arg(long)]\r\n        once: bool,\r","truncated":true,"truncatedBy":"lines","totalLines":18847,"totalBytes":4216,"outputLines":85,"outputBytes":4216,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"struct Cli {\n    /// Emit machine-readable JSON instead of the human view. Honored by the\r\n    /// read/status commands (list, whoami, status, description, role, the *-list\r\n    /// queries, how-to); action commands ignore it.\r\n    // [impl->REQ-CLI-JSON]\r\n    #[arg(long, global = true)]\r\n    json: bool,\r\n    // Optional so a BARE `spt` (no subcommand) parses to `None` instead of a\r\n    // clap \"required subcommand\" error — bare `spt` opens the endpoint-run\r\n    // picker on an interactive terminal (REQ-RUN-PICKER). `--help`/`-h`/`help`\r\n    // are handled by clap before we ever see `None`.\r\n    #[command(subcommand)]\r\n    cmd: Option<Cmd>,\r\n}\r\n\r\n#[derive(Subcommand)]\r\nenum Cmd {\r\n    /// Send a message (body read from stdin); fire-and-forget.\r\n    Send {\r\n        // [impl->REQ-SEND-REPLYTO-REMOVE] `--reply-to` is hard-removed; `target` is\r\n        // a required positional (no reply-to fallback). Reply-correlation rides the\r\n        // structural `from`, not a flag.\r\n        /// Target perch id.\r\n        target: String,\r\n        /// Sender id carried structurally as the message `from` (auto-detected from session if omitted).\r\n        #[arg(long)]\r\n        from: Option<String>,\r\n        /// Deliver only when the target is idle (the idle/wake window); hold\r\n        /// until then and never surface to the target's active poll.\r\n        #[arg(long, conflicts_with = \"active_only\")]\r\n        idle_only: bool,\r\n        /// Deliver only through the target's own poll (the no-interrupt hook\r\n        /// channel); never wakes an idle target. Replaces the old --deferred.\r\n        // `alias = \"deferred\"` is a HIDDEN back-compat alias: `--deferred` shipped\r\n        // as a documented flag in published reference.md, so `spt send --deferred`\r\n        // keeps parsing (→ active_only) for external scripts/adapters. Hidden (not\r\n        // `visible_alias`) → it does NOT render in the generated reference.md, so\r\n        // there is zero docs-drift. (ADR-0028 W2 pure-rename + back-compat.)\r\n        #[arg(long, alias = \"deferred\")]\r\n        active_only: bool,\r\n        /// Drop the message if it cannot be delivered in its window, instead of\r\n        /// spooling until delivered.\r\n        #[arg(long)]\r\n        ephemeral: bool,\r\n        /// Deliver through the target's translation binary when one is running,\r\n        /// else fall back to the normal channel. Delivers regardless of idle/active.\r\n        #[arg(long, conflicts_with = \"force_native\")]\r\n        prefer_native: bool,\r\n        /// Deliver ONLY through the target's translation binary — no fallback and\r\n        /// no spooling. If no binary is running the send is reported undelivered.\r\n        #[arg(long)]\r\n        force_native: bool,\r\n        /// Attach an opaque JSON metadata blob alongside the message body, carried\r\n        /// verbatim for the receiving adapter to parse. Does not replace the body.\r\n        #[arg(long, value_name = \"JSON\")]\r\n        json_payload: Option<String>,\r\n        /// Request the `user-msg` type (the user's authority). Honored only from\r\n        /// a user-backed origin (a Gateway endpoint, or the local user's own\r\n        /// CLI); an agent-family sender is re-stamped to plain `msg`.\r\n        #[arg(long = \"user-msg\")]\r\n        user_msg: bool,\r\n    },\r\n    /// Send and block for a reply (body read from stdin).\r\n    ///\r\n    /// The reply body is printed to stdout; gives up after --timeout seconds.\r\n    Ring {\r\n        /// Target perch id.\r\n        target: String,\r\n        /// Sender id (auto-detected from session if omitted).\r\n        #[arg(long)]\r\n        from: Option<String>,\r\n        /// Seconds to wait for a reply before giving up.\r\n        #[arg(long, default_value = \"60\")]\r\n        timeout: u64,\r\n    },\r\n    /// Become reachable: register the perch and listen (blocks).\r\n    ///\r\n    /// Drains the spooled backlog first; each received message prints to\r\n    /// stdout. With --once, runs a single drain+receive cycle and exits.\r\n    Ready {\r\n        /// This agent's perch id.\r\n        id: String,\r\n        /// Run a single drain+receive cycle, then exit (one-shot fallback\r\n        /// for harnesses that cannot host a long-running listener).\r\n        #[arg(long)]\r\n        once: bool,\r\n…\n    },\n…\n}","startLine":53,"lineNumbers":[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,null,143,null,310]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\cli.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":18900,"totalBytes":4216,"outputLines":85,"outputBytes":4216,"shownRange":{"start":54,"end":138},"nextOffset":139}}},"isError":false,"timestamp":1784197517077}}
{"type":"message","id":"ddf35e6e","parentId":"27cb5c83","timestamp":"2026-07-16T10:25:17.092Z","message":{"role":"toolResult","toolCallId":"call_9kfoGeuSMnDaXfsdzD9O5CnR|fc_017bff223290b7ba016a58b18cd768819a8af3bdeee452fd6b","toolName":"grep","content":[{"type":"text","text":"# CHANGELOG.md\n 1001|\n*1002|- **`spt subnet revoke <node>…`** — remove one or more machines from a subnet across the **whole fleet**, not just locally. It tells every member to drop the node within moments, then rotates the subnet's shared secret so the removed machine is locked out and must re-pair to come back. By default the rotation is batched at the end of a one-hour window — several revokes in that window share a single rotation, and any member that was briefly offline heals automatically across it. Pass `--force-rotate...\n 1003|- **`spt daemon start`** — bring the daemon up in the background, idempotently. When `spt` is installed as a service (the Linux per-user service, or the Windows logon task), `start` and `stop` now drive *that* service instead of a stray hand-started daemon — so the two never fight each other for the connection. `spt daemon start` on an already-running daemon just says so and does nothing.\n 1004|\n 1005|### Changed\n\n# docs/\n## KNOWN-HAZARDS.md\n 215|- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill ...\n*216|- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run`...\n 217|- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n*218|- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n 219|\n 220|<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n 221|### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n...\n 639|<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\n*640|- **Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc<Mutex<SendHalf>>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefi...\n 641|- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On dead...\n*642|- **spt-core mapping:** `spt-daemon/src/conn.rs` `BrokerConn` (Condvar-gated serialized write gate + poisoned state + per-in-flight op id + watchdog + `abort_physical()`: cfg(windows) `CancelIoEx` → completion handshake → `DisconnectNamedPipe` raw kernel32 externs; cfg(unix) `UnixStream::shutdown(Both)`); `broker.rs` `SharedSend = Arc<BrokerConn>` + `controller_writer`/`viewer_writer`/`send_frame`/`send_error` routed; `nethost.rs` stream-log/presence sends routed. int = `brain_decouple.rs` `non_drain...\n*643|- **Source:** UPDATE-WEDGE round-4 (hertz root doc `docs/UPDATE-WEDGE-ROOT-CAUSE.md` @f8596ca; doyle gate ruling `docs/UPDATE-WEDGE-RCA-DISPATCH-todlando.md`; todlando build 2026-07-09). The residual 7.36 left open: logical eviction without physical retirement.\n 644|\n 645|---\n 646|\n...\n 665|### 7.41 A fresh dispatcher must NEVER re-serve a terminal stream — historical replay must not steal or clear a live controller  `[REQ-HAZARD-REDISPATCH-CONTROL-STEAL]`\n*666|- **Failure (paid-for, hertz field RCA 2026-07-16 — 4/5 endpoints frozen, twice in one day):** every brain cycle (`spt daemon refresh`, `spt update` apply — same `applyhost.rs` path) launches a fresh `dispatch::run_dispatch_loop` whose EMPTY per-process `claimed` set enumerates every broker-held peer stream. `NetShared.streams` has NO removal path (`StreamLog::finish` only marks) and the claim condition has no finished filter → the fresh dispatcher replays FINISHED historical attach streams. A repl...\n 667|- **Invariant:** redispatch eligibility is LIFECYCLE-gated: a finished/terminal stream row is retired from the dispatcher's enumeration (a fresh dispatcher never re-serves a terminal Attach); classification identity is restart-durable and independent of the evictable data ring (immutable opener fact pinned per stream until close); claims are retryable on transient worker-setup failure and terminal on terminal outcomes (no hot-loop). The legitimate same-`by` successor re-take after a brain restart still s...\n 668|- **spt-core mapping:** `spt-daemon/src/dispatch.rs` `run_dispatch_loop` claim condition + `peek_first_line` classification + worker spawn; `spt-daemon/src/nethost.rs` `NetShared.streams` insert/finish + `DEFAULT_STREAM_RING_CHUNKS`; `spt-daemon/src/applyhost.rs` (the brain-cycle trigger).\n*669|- **Source:** REDISPATCH-TRUTH W1 (hertz source-level RCA `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`, doyle seam-verified; ADR-0038; distinct from the CLOSED 0.30.x session-cursor resume-steal saga).\n 670|\n 671|---\n 672|\n## MSG-IDENTITY-DISPATCH.md\n 31|\n*32|### W2 — BRINGUP-BIND (F-034 legs a/b/c; hertz /sptc:live cascade)\n 33|- `REQ-ADAPTER-UNRESOLVED-HINT-FORM` [impl,unit] — hint prints the clap-valid group-level\n 34|  `--adapter` form. Trivial.\n 35|- `REQ-LISTEN-SEED-CONSUME-AFTER-BIND` [doc,impl,unit,int] — validate + BIND first, consume the\n...\n 39|  the ephemeral seed is gone (late go-live / post-daemon-restart). Doc stage pins seed lifetime.\n*40|- Roots pinned (scope F-034-sptc-live-core-legs.md). Field-verify: hertz + perri.\n 41|\n 42|### W3 — INGEST-DURABILITY (F-032; data-loss)\n 43|- `REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE` [impl,unit,int] — never delete a drop whose slice\n## PROJECT-INDEX-TRIAGE.md\n 3|> Doyle triage 2026-07-15, dispatched at the v0.32.0 boundary (operator pick).\n*4|> Canonical RCA: `.claude/reports/2026-07-10-hertz-session/03-cli-project-index-latency-rca.md`\n*5|> (hertz, root pinned, architecture agreed with operator). Decision record:\n*6|> ADR-0037. Lane: doyle triages → todlando builds → doyle gates → hertz\n 7|> field-verify partner.\n 8|\n 9|## Why (two independent motivators, one root)\n...\n 75|- Manual latency acceptance on the 13-perch/7-branch fixture (target:\n*76|  ~30s → sub-second) + hertz field-verify on HFENDULEAM.\n 77|\n 78|## Gate rulings (pre-made, RCA §Regression tests — binding at gate)\n 79|\n## REDISPATCH-TRUTH-TRIAGE.md\n 3|> Doyle triage 2026-07-16, dispatched at the v0.33.0 boundary (ranked-first\n*4|> from the hertz same-day RCA; displaces MESH-RECOVERY). Canonical RCA:\n*5|> `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`\n*6|> (hertz source-level, doyle seam-verified same day). Decision record:\n 7|> ADR-0038. Lane: doyle triages → todlando builds → doyle gates → deployah\n*8|> releases → hertz field-verify (they hold the incident logs).\n 9|\n 10|## Why (field: 4/5 endpoints frozen on one refresh, twice in one day)\n 11|\n...\n 38|\n*39|Field evidence (HFENDULEAM, hertz preserves\n 40|`AppData/Local/spt-core/logs/daemon.stderr.log{,.1}`, brain generations\n 41|2/3): 4/5 endpoints frozen across both 2026-07-16 doyle refreshes\n 42|(deployah-recovery + the v0.33.0 apply) + lia on the update;\n...\n 54|\n*55|## Shape (hertz A–D, doyle-endorsed; ADR-0038)\n 56|\n 57|Broker stream-table rows gain explicit **lifecycle truth**, and the\n 58|dispatcher redispatches only what is genuinely live:\n...\n 90|\n*91|Then: doyle gates → deployah releases → hertz field-verify on HFENDULEAM\n 92|(daemon refresh with active remote `spt rc` sessions: no frozen PTYs, no\n 93|historical `DISPATCH:N:Failed` replay storm in the fresh generation).\n 94|\n\n## adr/\n### 0037-materialized-project-index.md\n 3|- **Status:** accepted (operator-agreed direction 2026-07-10, dispatched 2026-07-15)\n*4|- **Context:** PROJECT-INDEX milestone; RCA `.claude/reports/2026-07-10-hertz-session/03-cli-project-index-latency-rca.md`\n 5|\n 6|## Context\n 7|\n### 0038-broker-stream-lifecycle-truth.md\n 2|\n*3|- **Status:** accepted (hertz RCA + doyle seam-verify 2026-07-16, dispatched same day)\n*4|- **Context:** REDISPATCH-TRUTH milestone; RCA `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`\n 5|\n 6|## Context\n 7|\n\nSkipped missing paths: .planning\n\n[Some lines truncated to 512 chars]"}],"details":{"scopePath":"docs, README.md, CHANGELOG.md","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":24,"fileCount":7,"files":["docs/KNOWN-HAZARDS.md","docs/MSG-IDENTITY-DISPATCH.md","docs/PROJECT-INDEX-TRIAGE.md","docs/REDISPATCH-TRUTH-TRIAGE.md","docs/adr/0037-materialized-project-index.md","docs/adr/0038-broker-stream-lifecycle-truth.md","CHANGELOG.md"],"fileMatches":[{"path":"docs/KNOWN-HAZARDS.md","count":7},{"path":"docs/MSG-IDENTITY-DISPATCH.md","count":2},{"path":"docs/PROJECT-INDEX-TRIAGE.md","count":4},{"path":"docs/REDISPATCH-TRUTH-TRIAGE.md","count":7},{"path":"docs/adr/0037-materialized-project-index.md","count":1},{"path":"docs/adr/0038-broker-stream-lifecycle-truth.md","count":2},{"path":"CHANGELOG.md","count":1}],"truncated":true,"displayContent":"# CHANGELOG.md\n 1001│\n*1002│- **`spt subnet revoke <node>…`** — remove one or more machines from a subnet across the **whole fleet**, not just locally. It tells every member to drop the node within moments, then rotates the subnet's shared secret so the removed machine is locked out and must re-pair to come back. By default the rotation is batched at the end of a one-hour window — several revokes in that window share a single rotation, and any member that was briefly offline heals automatically across it. Pass `--force-rotate...\n 1003│- **`spt daemon start`** — bring the daemon up in the background, idempotently. When `spt` is installed as a service (the Linux per-user service, or the Windows logon task), `start` and `stop` now drive *that* service instead of a stray hand-started daemon — so the two never fight each other for the connection. `spt daemon start` on an already-running daemon just says so and does nothing.\n 1004│\n 1005│### Changed\n\n# docs/\n## KNOWN-HAZARDS.md\n 215│- **Failure:** membership-implies-reachability made *every* `spt` invocation a potential daemon spawner (`ensure_running`), including the elevation-gated ones (`subnet create`/`join`, REQ-SUBNET-4). The spawned daemon inherits the spawner's token. **Windows:** an elevated `subnet create` auto-starts an ELEVATED daemon whose named pipes deny unelevated clients — every subsequent unelevated `spt` reads \"not running\", tries to spawn its own daemon, and dies on bind Access-denied; the user had to taskkill ...\n*216│- **Invariant:** the daemon **always runs unelevated in the invoking user's universe**, regardless of which command spawns it. Two enforcement points sharing one seam: (a) `spawn_detached` de-elevates the child — Windows: the UAC **linked token** (`TokenLinkedToken` → `DuplicateTokenEx` → `CreateProcessWithTokenW`; inherits no handles, so 5.6 holds by construction); Linux: drop to `SUDO_UID`/`SUDO_GID` with `$HOME`/`$USER`/`$LOGNAME` reset to the invoking user's (passwd lookup); (b) a `Daemon::run`...\n 217│- **spt-core mapping:** `spt-daemon::deelevate` (the OS-split seam) consumed by `daemon::spawn_detached` + the `Daemon::run` entry guard. The fuller Linux elevation model (install symlink + default-account election) is deferred (DEFERRED.md, M8).\n*218│- **Source:** M7 acceptance run 2026-06-06 (DEFERRED.md \"Non-admin daemon spawn\"); interim field rule was \"bring the daemon up unelevated FIRST\".\n 219│\n 220│<!-- [doc->REQ-HAZARD-CHILD-CONSOLE-FLASH] -->\n 221│### 5.8 Console children of the console-less daemon flash visible windows  `[REQ-HAZARD-CHILD-CONSOLE-FLASH]`\n    │...\n 639│<!-- [doc->REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK] -->\n*640│- **Failure (paid-for, field capture + deterministic Windows repro 2026-07-09 — hertz RCA, UPDATE-WEDGE round-4):** `controller_writer` held the `SharedSend = Arc<Mutex<SendHalf>>` guard ACROSS a blocking `write_frame`. On Windows, `interprocess 2.4.2` routes the send to `WriteFileEx` + `SleepEx(INFINITE, alertable)` — this local-socket send path exposes **no supported write timeout** (`set_timeout` → `Unsupported`). An `rc --take` controller consumer that stopped reading blocked the write **indefi...\n 641│- **Invariant:** every write on a physical broker connection rides ONE broker-owned bounded/cancelable framed-write primitive with an **independent out-of-band aborter**. The `SendHalf` never leaves the conn object (a bypass cannot compile). An ABSOLUTE deadline stamped at write entry covers BOTH the serialized gate-wait AND OS write completion; a per-conn watchdog fires at that deadline and aborts the in-flight op — never relying on the write returning or on opportunistic stall-evict sampling. On dead...\n*642│- **spt-core mapping:** `spt-daemon/src/conn.rs` `BrokerConn` (Condvar-gated serialized write gate + poisoned state + per-in-flight op id + watchdog + `abort_physical()`: cfg(windows) `CancelIoEx` → completion handshake → `DisconnectNamedPipe` raw kernel32 externs; cfg(unix) `UnixStream::shutdown(Both)`); `broker.rs` `SharedSend = Arc<BrokerConn>` + `controller_writer`/`viewer_writer`/`send_frame`/`send_error` routed; `nethost.rs` stream-log/presence sends routed. int = `brain_decouple.rs` `non_drain...\n*643│- **Source:** UPDATE-WEDGE round-4 (hertz root doc `docs/UPDATE-WEDGE-ROOT-CAUSE.md` @f8596ca; doyle gate ruling `docs/UPDATE-WEDGE-RCA-DISPATCH-todlando.md`; todlando build 2026-07-09). The residual 7.36 left open: logical eviction without physical retirement.\n 644│\n 645│---\n 646│\n    │...\n 665│### 7.41 A fresh dispatcher must NEVER re-serve a terminal stream — historical replay must not steal or clear a live controller  `[REQ-HAZARD-REDISPATCH-CONTROL-STEAL]`\n*666│- **Failure (paid-for, hertz field RCA 2026-07-16 — 4/5 endpoints frozen, twice in one day):** every brain cycle (`spt daemon refresh`, `spt update` apply — same `applyhost.rs` path) launches a fresh `dispatch::run_dispatch_loop` whose EMPTY per-process `claimed` set enumerates every broker-held peer stream. `NetShared.streams` has NO removal path (`StreamLog::finish` only marks) and the claim condition has no finished filter → the fresh dispatcher replays FINISHED historical attach streams. A repl...\n 667│- **Invariant:** redispatch eligibility is LIFECYCLE-gated: a finished/terminal stream row is retired from the dispatcher's enumeration (a fresh dispatcher never re-serves a terminal Attach); classification identity is restart-durable and independent of the evictable data ring (immutable opener fact pinned per stream until close); claims are retryable on transient worker-setup failure and terminal on terminal outcomes (no hot-loop). The legitimate same-`by` successor re-take after a brain restart still s...\n 668│- **spt-core mapping:** `spt-daemon/src/dispatch.rs` `run_dispatch_loop` claim condition + `peek_first_line` classification + worker spawn; `spt-daemon/src/nethost.rs` `NetShared.streams` insert/finish + `DEFAULT_STREAM_RING_CHUNKS`; `spt-daemon/src/applyhost.rs` (the brain-cycle trigger).\n*669│- **Source:** REDISPATCH-TRUTH W1 (hertz source-level RCA `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`, doyle seam-verified; ADR-0038; distinct from the CLOSED 0.30.x session-cursor resume-steal saga).\n 670│\n 671│---\n 672│\n## MSG-IDENTITY-DISPATCH.md\n 31│\n*32│### W2 — BRINGUP-BIND (F-034 legs a/b/c; hertz /sptc:live cascade)\n 33│- `REQ-ADAPTER-UNRESOLVED-HINT-FORM` [impl,unit] — hint prints the clap-valid group-level\n 34│  `--adapter` form. Trivial.\n 35│- `REQ-LISTEN-SEED-CONSUME-AFTER-BIND` [doc,impl,unit,int] — validate + BIND first, consume the\n   │...\n 39│  the ephemeral seed is gone (late go-live / post-daemon-restart). Doc stage pins seed lifetime.\n*40│- Roots pinned (scope F-034-sptc-live-core-legs.md). Field-verify: hertz + perri.\n 41│\n 42│### W3 — INGEST-DURABILITY (F-032; data-loss)\n 43│- `REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE` [impl,unit,int] — never delete a drop whose slice\n## PROJECT-INDEX-TRIAGE.md\n  3│> Doyle triage 2026-07-15, dispatched at the v0.32.0 boundary (operator pick).\n *4│> Canonical RCA: `.claude/reports/2026-07-10-hertz-session/03-cli-project-index-latency-rca.md`\n *5│> (hertz, root pinned, architecture agreed with operator). Decision record:\n *6│> ADR-0037. Lane: doyle triages → todlando builds → doyle gates → hertz\n  7│> field-verify partner.\n  8│\n  9│## Why (two independent motivators, one root)\n   │...\n 75│- Manual latency acceptance on the 13-perch/7-branch fixture (target:\n*76│  ~30s → sub-second) + hertz field-verify on HFENDULEAM.\n 77│\n 78│## Gate rulings (pre-made, RCA §Regression tests — binding at gate)\n 79│\n## REDISPATCH-TRUTH-TRIAGE.md\n  3│> Doyle triage 2026-07-16, dispatched at the v0.33.0 boundary (ranked-first\n *4│> from the hertz same-day RCA; displaces MESH-RECOVERY). Canonical RCA:\n *5│> `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`\n *6│> (hertz source-level, doyle seam-verified same day). Decision record:\n  7│> ADR-0038. Lane: doyle triages → todlando builds → doyle gates → deployah\n *8│> releases → hertz field-verify (they hold the incident logs).\n  9│\n 10│## Why (field: 4/5 endpoints frozen on one refresh, twice in one day)\n 11│\n   │...\n 38│\n*39│Field evidence (HFENDULEAM, hertz preserves\n 40│`AppData/Local/spt-core/logs/daemon.stderr.log{,.1}`, brain generations\n 41│2/3): 4/5 endpoints frozen across both 2026-07-16 doyle refreshes\n 42│(deployah-recovery + the v0.33.0 apply) + lia on the update;\n   │...\n 54│\n*55│## Shape (hertz A–D, doyle-endorsed; ADR-0038)\n 56│\n 57│Broker stream-table rows gain explicit **lifecycle truth**, and the\n 58│dispatcher redispatches only what is genuinely live:\n   │...\n 90│\n*91│Then: doyle gates → deployah releases → hertz field-verify on HFENDULEAM\n 92│(daemon refresh with active remote `spt rc` sessions: no frozen PTYs, no\n 93│historical `DISPATCH:N:Failed` replay storm in the fresh generation).\n 94│\n\n## adr/\n### 0037-materialized-project-index.md\n 3│- **Status:** accepted (operator-agreed direction 2026-07-10, dispatched 2026-07-15)\n*4│- **Context:** PROJECT-INDEX milestone; RCA `.claude/reports/2026-07-10-hertz-session/03-cli-project-index-latency-rca.md`\n 5│\n 6│## Context\n 7│\n### 0038-broker-stream-lifecycle-truth.md\n 2│\n*3│- **Status:** accepted (hertz RCA + doyle seam-verify 2026-07-16, dispatched same day)\n*4│- **Context:** REDISPATCH-TRUTH milestone; RCA `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`\n 5│\n 6│## Context\n 7│","missingPaths":[".planning"],"linesTruncated":true,"meta":{"limits":{"columnTruncated":{"maxColumn":512}}}},"isError":false,"timestamp":1784197517092}}
{"type":"message","id":"6afd8e35","parentId":"ddf35e6e","timestamp":"2026-07-16T10:25:17.099Z","message":{"role":"toolResult","toolCallId":"call_Wlw5EiwnwBxjEdnmQSWjFlg6|fc_017bff223290b7ba016a58b18cd77c819a89df0976e3df917a","toolName":"grep","content":[{"type":"text","text":"# crates/\n\n## spt/\n\n### tests/\n#### active_only_never_relay_e2e.rs\n 84|            controller_node: None,\n*85|            harness_only: false,\n 86|            adapter: None,\n 87|            recent_projects: Vec::new(),\n 88|            controlled: false,\n#### contract_e2e.rs\n 8|//!\n*9|//! A second test exercises the **harness-hosted startup** topology proper —\n*10|//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered\n 11|//! message — the REQ-START-2 path.\n 12|//\n 13|// [int->REQ-API-1]\n...\n 180|\n*181|/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed\n 182|/// (matched by parent pid, validated against pid-recycle), binds the perch\n 183|/// skeleton→live, and relays a delivered message.\n 184|#[test]\n...\n 233|        .output()\n*234|        .expect(\"run api listen\");\n 235|    assert!(\n 236|        listen.status.success(),\n*237|        \"api listen exit: {:?}\",\n 238|        listen.status\n 239|    );\n 240|    let stdout = String::from_utf8_lossy(&listen.stdout);\n...\n 367|        .output()\n*368|        .expect(\"run api listen\");\n 369|    let listen_err = String::from_utf8_lossy(&listen.stderr);\n 370|    assert!(\n 371|        listen.status.success(),\n*372|        \"api listen exit: {:?}\\n{listen_err}\",\n 373|        listen.status\n 374|    );\n 375|\n...\n 472|/// `spt api seed` transparently spins up `spt daemon`, the seed lands in its\n*473|/// in-memory map (no file), and a follow-up `api listen` TAKEs it and binds —\n 474|/// proving the cross-invocation handoff rides the daemon, not a seed file.\n 475|#[test]\n 476|fn cold_api_call_autostarts_daemon_and_handoff_is_in_memory() {\n...\n 534|        .output()\n*535|        .expect(\"run api listen\");\n 536|    let listen_ok = listen.status.success();\n 537|    let stdout = String::from_utf8_lossy(&listen.stdout).into_owned();\n 538|    let rec = spt_store::info::read_info(&perch_path);\n...\n 546|\n*547|    assert!(listen_ok, \"api listen exit: {:?}\", listen.status);\n 548|    assert!(\n 549|        stdout.contains(\"delivered after cold start\"),\n 550|        \"listen must relay the spooled message via the in-memory seed handoff, got: {stdout:?}\"\n#### dummy_harness_e2e.rs\n 245|    // attach-CONNECTS proof (Wave 1 scope): the rc pump spawned its reader and did\n*246|    // NOT RC_FAIL (no \"no live session\" / loopback-dial refusal).\n 247|    let rc_connected = rc_stderr.contains(\"PUMP_IPC_READER\") && !rc_stderr.contains(\"RC_FAIL\");\n 248|    eprintln!(\n 249|        \"=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \\\n...\n 329|// offline status-gate (\"offline — nothing to attach\", REQ-HAZARD-RC-ATTACH-FAILFAST\n*330|// in rc.rs run_attach) or hits \"no live session for endpoint\" (printed as\n 331|// RC_FAIL) — never connecting to the live PTY, so NO heartbeat is ever seen.\n 332|//\n 333|// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live\n...\n 465|    // (4c) Drive the endpoint OFFLINE: kill the first harness incarnation. Its broker\n*466|    //      session then drains and the next reconcile sweep sees no live session for a\n 467|    //      dead, online-latched, controllable perch → marks it offline.\n 468|    if let Some(p) = harness_pid_1 {\n 469|        kill_pid(p);\n...\n 538|    let bailed_no_session =\n*539|        run_captured.contains(\"no live session\") || run_stderr_txt.contains(\"no live session\");\n 540|\n 541|    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\n 542|    eprintln!(\n#### idle_edge_drain_e2e.rs\n 6|//! DEFERRED (window=`active_only`) awaiting a hook poll. An idle spt-hosted session\n*7|//! (no `api listen` relay to wake it) may never poll — so on the ACTIVE→IDLE edge\n 8|//! the daemon must DRAIN that pending spool (deferred AND non-deferred) through the\n 9|//! ONE shared spt-hosted inject leg (`try_spt_hosted_inject`), the same leg WAN\n 10|//! ingress and local `spt send` use. Pre-leg-2 the idle-transition drain\n#### list_json_liveness_parity_e2e.rs\n 81|            controller_node: None,\n*82|            harness_only: false,\n 83|            adapter: None,\n 84|            recent_projects: Vec::new(),\n 85|            controlled: false,\n#### listen_seed_retry_e2e.rs\n 5|//! 1. **Seed survives a pre-bind refusal** (REQ-LISTEN-SEED-CONSUME-AFTER-BIND,\n*6|//!    hertz's headline): on a multi-subnet node an `api listen` with no `--subnet`\n 7|//!    refuses HOME AFTER it has already TAKEn the consume-once seed. The corrected\n 8|//!    retry on the SAME pid (adding `--subnet`) must still find the seed and BIND —\n 9|//!    never the NO_SEED dead end the field bug hit.\n...\n 11|//!    ephemeral seed is GONE (late go-live / post-daemon-restart) binds via\n*12|//!    `api listen <id> --session-id <sid>` — the loud SID_BIND marker path.\n 13|//!\n 14|//! Both drive the real binary with an explicit `--adapter mock` (bin-only crate:\n 15|//! the override is the deterministic no-registry path the sibling contract_e2e.rs\n...\n 66|// [int->REQ-LISTEN-SEED-CONSUME-AFTER-BIND] F-034 leg b: a pre-bind HOME refusal on\n*67|// a multi-subnet node must NOT burn the consume-once seed. The first `api listen`\n 68|// (no --subnet) TAKEs the seed then refuses HOME; the corrected retry on the SAME\n 69|// pid (adding --subnet) still finds the seed and BINDs — the NO_SEED dead end is\n 70|// gone. Proven through the real binary + real daemon IPC, no mock shim.\n...\n 107|        .output()\n*108|        .expect(\"run first api listen\");\n 109|    let refused_err = String::from_utf8_lossy(&refused.stderr);\n 110|    assert!(\n 111|        !refused.status.success(),\n...\n 136|        .output()\n*137|        .expect(\"run retry api listen\");\n 138|    let retry_err = String::from_utf8_lossy(&retry.stderr);\n 139|    let retry_out = String::from_utf8_lossy(&retry.stdout);\n 140|    assert!(\n...\n 167|// (its ephemeral SessionStart seed expired / a daemon restart dropped it) binds via\n*168|// `api listen <id> --session-id <sid>`. The seed path yields NO_SEED, the fallback\n 169|// binds from the sid (loud SID_BIND marker), and the perch carries the sid. This is\n 170|// the post-daemon-restart shape — the seed is simply absent.\n 171|#[test]\n...\n 196|        .output()\n*197|        .expect(\"run api listen --session-id\");\n 198|    let listen_err = String::from_utf8_lossy(&listen.stderr);\n 199|    let listen_out = String::from_utf8_lossy(&listen.stdout);\n 200|    assert!(\n#### live_firsthost_e2e.rs\n 4|//!\n*5|//! a live-capable `spt api listen --once` BINDS the Self perch and marks it\n 6|//! `status=online` (the W0.2 first-host handoff — the Psyche no longer spawns in\n 7|//! this process) → the brain's `livehost::reconcile_once` then HOSTS the Psyche\n 8|//! lifecycle off that real online status → a graceful offline transition un-hosts\n...\n 34|\n*35|/// In-process seed daemon so `api seed`/`api listen` succeed without a REAL\n 36|/// detached `spt daemon` (shell_e2e's pattern).\n 37|fn start_inproc_daemon() {\n 38|    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n...\n 120|        Some(STATUS_ONLINE),\n*121|        \"a live `api listen` marks the perch online (the first-host handoff)\"\n 122|    );\n 123|\n 124|    // ── the brain reconcile hosts off that REAL online status ──\n#### live_resolve_e2e.rs\n 1|//! v0.9.0 bind-time adapter resolution int (REQ-START-5 / ADR-0021): the REAL\n*2|//! `spt api seed` (adapter-agnostic) → `spt api listen` (no `--adapter`) bringup,\n 3|//! proving the /sptc:live parity fix end-to-end:\n 4|//!\n 5|//! - the seed carries NO `--adapter`; `listen` with NO `--adapter` resolves the\n#### livehost_bootgate_e2e.rs\n 16|//! brain over a net-less node, hand-stamps exactly such a stale perch (the cold-start\n*17|//! leftover — there is deliberately NO `api listen`/`bind`, so the broker hosts no\n 18|//! session for it), and asserts BOTH: the perch is flipped OFFLINE at boot AND the\n 19|//! nested `{id}-psyche` perch NEVER appears (no phantom revival).\n 20|//!\n#### ready_resume_ledger_e2e.rs\n*1|//! REQ-READY-AGENT-RESUME int — a harness-hosted READY endpoint (the `spt ready`\n 2|//! bind, `ReadyAgent::start_homed`) now LEDGERS a Boot session row, so an OFFLINE\n 3|//! ready perch carries resume rows and the picker's offline Resume-from-history\n 4|//! offers it (closing the gap that today only live agents enjoyed) — AND the daemon\n...\n 105|        .spawn()\n*106|        .expect(\"spawn `spt ready <id>` (the harness-hosted ready bind)\");\n 107|\n 108|    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 109|    let deadline = Instant::now() + Duration::from_secs(25);\n\n### src/\n#### rc.rs\n 1127|    // ABSENT or status-less perch falls through to the broker path, which already\n*1128|    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n 1129|    // is NOT used here: it returns true for an absent info.json by interim parity.)\n 1130|    //\n 1131|    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n...\n 1296|            // The serve refused before anything ran (#4, REQ-RC-CROSS-NODE-ATTACH):\n*1297|            // no live session for the endpoint on the node we reached, or its gate\n 1298|            // denied us. On the cross-node leg this is the D6 stale-row case.\n 1299|            match &remote_node {\n 1300|                Some(node) => {\n...\n 1302|                        stdout,\n*1303|                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\n 1304|                         have ended, or the registry row is stale]\"\n 1305|                    );\n 1306|                }\n 1307|                None => {\n*1308|                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\n 1309|                }\n 1310|            }\n 1311|            Ok(())\n...\n 1351|enum EstablishFail {\n*1352|    /// No attachable target (no live session anywhere / ambiguous / node\n 1353|    /// unreachable) — the carried message is the CLEAN first-attempt exit copy;\n 1354|    /// the reconnect loop retries it inside the window instead (the node may\n 1355|    /// still be coming back).\n...\n 1438|                return Err(EstablishFail::NoTarget(format!(\n*1439|                    \"No live session for '{endpoint_id}' — not running on this node \\\n 1440|                     or any visible subnet.\"\n 1441|                )))\n 1442|            }\n...\n 1684|/// SEVERED under a live viewport (reconnect); nothing ever rendered ⇒ the serve\n*1685|/// REFUSED the attach (no live session / gate deny — the cross-node D6\n*1686|/// stale-row case), honest \"no live session\" instead of a false \"detached\"\n 1687|/// (#4, REQ-RC-CROSS-NODE-ATTACH).\n 1688|// [impl->REQ-RC-RECONNECT]\n 1689|fn serve_eof_end(rendered_any: bool) -> PumpEnd {\n...\n 1753|    /// The serve side finished the stream having produced NO output — i.e. it\n*1754|    /// refused the attach (no live session for the endpoint, or the access gate\n 1755|    /// denied us). On the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH) this is\n 1756|    /// the D6 stale-row case: the registry routed us to a node that no longer has\n 1757|    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\n 1758|    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\n*1759|    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n 1760|    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n 1761|    NoLiveSession,\n 1762|}\n...\n 1865|    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n*1866|    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n 1867|    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n 1868|    // Any live session replays its buffered ring on attach, so a real attach sets\n 1869|    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n\n## spt-daemon/\n\n### tests/\n#### daemon_e2e.rs\n 10|//! 2. **Psyche-loop** — a [`BrainLifecycle`] (the consolidated re-host of the\n*11|//!    interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;\n 12|//!    the daemon marks the hosted perch `online`, so its liveness is the daemon\n 13|//!    `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).\n 14|//! 3. **commune** — a commune drop lands in the watched dir and a scheduled pulse\n#### digest.rs\n 3|//!\n*4|//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a\n 5|//! Claude-Code-style per-session log is the worked example). The daemon projects\n 6|//! its digest **on demand** from the adapter's `[digest]` **extractor** (ADR-0019:\n 7|//! the digest's own seam, no longer riding the opaque `[history]` normalizer) run\n...\n 44|/// Register a harness adapter whose `[digest]` extractor reads `fixture` (a\n*45|/// digest-record JSONL), and establish a harness-hosted perch for `id` that\n 46|/// records the adapter — the daemon resolves the projection source from these two\n 47|/// facts alone (no broker, no live feed).\n 48|fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {\n...\n 120|    .expect(\"pull ok\")\n*121|        .expect(\"a digest projects for the harness-hosted endpoint\");\n 122|    assert!(version >= 1, \"the projection has a version\");\n 123|    assert_eq!(digest.turns.len(), 1, \"one user turn: {digest:?}\");\n 124|    assert_eq!(digest.turns[0].input.as_deref(), Some(\"add a file\"));\n#### dispatch.rs\n 529|// the attach landed on the right session without A ever naming a session id.\n*530|// A named endpoint with NO live session on B (the D6 stale-registry-row case)\n 531|// refuses cleanly: the stream finishes with no output, never a false attach.\n 532|#[test]\n 533|fn dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint() {\n...\n 618|\n*619|    // A STALE endpoint (no live session on B) refuses: clean finish, no output.\n 620|    let ghost = spt_daemon::attach::request_attach_endpoint(\n 621|        &mut a,\n 622|        conn.conn_id,\n...\n 646|                } if stream_id == ghost && !bytes.is_empty() => {\n*647|                    panic!(\"a stale endpoint with no live session must never serve output\");\n 648|                }\n 649|                _ => {}\n 650|            }\n...\n 654|        refused,\n*655|        \"a stale endpoint with no live session refuses with a clean stream finish\"\n 656|    );\n 657|    stop.store(true, Ordering::Relaxed);\n 658|}\n...\n 754|            controller_node: None,\n*755|            harness_only: false,\n 756|            adapter: None,\n 757|            recent_projects: Vec::new(),\n 758|            controlled: false,\n...\n 1077|            controller_node: None,\n*1078|            harness_only: false,\n 1079|            adapter: None,\n 1080|            recent_projects: Vec::new(),\n 1081|            controlled: false,\n#### inject_control_wedge.rs\n 2849|// a cross-node `spt send` funnels into `receive_wan`, which today tries the\n*2850|// harness-hosted TCP relay leg (`deliver_tcp`) then falls straight to SPOOL — it\n 2851|// has NO spt-hosted broker-inject leg (that leg exists ONLY in local cmd_send /\n 2852|// `drain_idle_window`). So a WAN arrival to a relay-less, spt-hosted perch with a\n 2853|// live, healthy translation binary sleeps in the spool FOREVER until some adapter\n#### pump.rs\n 356|                    controller_node: None,\n*357|                    harness_only: false,\n 358|                    adapter: None,\n 359|                    recent_projects: Vec::new(),\n 360|                    controlled: false,\n#### replicate.rs\n 127|        controller_node: None,\n*128|        harness_only: false,\n 129|        adapter: None,\n 130|        recent_projects: Vec::new(),\n 131|        controlled: false,\n...\n 166|            controller_node: None,\n*167|            harness_only: false,\n 168|            adapter: None,\n 169|            recent_projects: Vec::new(),\n 170|            controlled: false,\n#### sync.rs\n 437|            controller_node: None,\n*438|            harness_only: false,\n 439|            adapter: None,\n 440|            recent_projects: Vec::new(),\n 441|            controlled: false,\n...\n 454|            controller_node: None,\n*455|            harness_only: false,\n 456|            adapter: None,\n 457|            recent_projects: Vec::new(),\n 458|            controlled: false,\n\n### src/\n#### inject.rs\n 29|/// Whether `target` is an spt-hosted endpoint whose PTY the broker holds, with NO\n*30|/// `api listen` relay: it is online, has no resolvable TCP relay address, and is\n*31|/// broker-PTY-`controllable`. A harness-hosted endpoint with a relay\n 32|/// (`resolve_address` is `Some`) takes the normal TCP path; an offline or\n 33|/// non-controllable perch is not a PTY-inject target. Factored verbatim from\n 34|/// `cmd_send`'s predicate so the WAN ingress shares ONE definition.\n...\n 196|    // controllable, relay-LESS endpoint is an spt-hosted inject target; a relay\n*197|    // address (harness-hosted), a non-controllable perch, or an offline one is not.\n 198|    #[test]\n 199|    fn shared_inject_leg_detects_spt_hosted() {\n 200|        crate::test_home::with_home(|_| {\n...\n 219|\n*220|            // Not controllable (harness-hosted live agent) → not an inject target.\n 221|            // [unit->REQ-LIVE-AGENT-NO-INJECT-DELIVERY] F-033 leg (a): a\n*222|            // harness-hosted live agent is structurally excluded from PTY-inject by\n*223|            // this predicate — bind_from_seed stamps controllable=Some(false), and\n 224|            // an unset (None) field is equally non-controllable. Both shapes below.\n 225|            let mut rec = info::read_info(&perch_path).unwrap();\n 226|            rec.controllable = None;\n...\n 233|            let mut rec = info::read_info(&perch_path).unwrap();\n*234|            rec.controllable = Some(false);\n 235|            info::write_info(&perch_path, &rec).unwrap();\n 236|            info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 237|            assert!(\n 238|                !is_spt_hosted_no_relay(id, &owlery),\n*239|                \"controllable=Some(false) (bind_from_seed's harness-hosted stamp) is not an inject target\"\n 240|            );\n 241|\n 242|            // Offline → not an inject target regardless.\n...\n 281|            );\n*282|            // A relay address (harness-hosted) → not an inject target → not the belt's job.\n 283|            let addr: std::net::SocketAddr = \"127.0.0.1:6001\".parse().unwrap();\n 284|            spt_store::registry::register_address(id, &addr, &owlery).unwrap();\n 285|            assert!("}],"details":{"scopePath":"crates/spt/tests, crates/spt-daemon/tests, crates/spt/src/rc.rs, crates/spt-daemon/src/inject.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":67,"fileCount":19,"files":["crates/spt/tests/active_only_never_relay_e2e.rs","crates/spt/tests/contract_e2e.rs","crates/spt/tests/dummy_harness_e2e.rs","crates/spt/tests/idle_edge_drain_e2e.rs","crates/spt/tests/list_json_liveness_parity_e2e.rs","crates/spt/tests/listen_seed_retry_e2e.rs","crates/spt/tests/live_firsthost_e2e.rs","crates/spt/tests/live_resolve_e2e.rs","crates/spt/tests/livehost_bootgate_e2e.rs","crates/spt/tests/ready_resume_ledger_e2e.rs","crates/spt-daemon/tests/daemon_e2e.rs","crates/spt-daemon/tests/digest.rs","crates/spt-daemon/tests/dispatch.rs","crates/spt-daemon/tests/inject_control_wedge.rs","crates/spt-daemon/tests/pump.rs","crates/spt-daemon/tests/replicate.rs","crates/spt-daemon/tests/sync.rs","crates/spt/src/rc.rs","crates/spt-daemon/src/inject.rs"],"fileMatches":[{"path":"crates/spt/tests/active_only_never_relay_e2e.rs","count":1},{"path":"crates/spt/tests/contract_e2e.rs","count":10},{"path":"crates/spt/tests/dummy_harness_e2e.rs","count":4},{"path":"crates/spt/tests/idle_edge_drain_e2e.rs","count":1},{"path":"crates/spt/tests/list_json_liveness_parity_e2e.rs","count":1},{"path":"crates/spt/tests/listen_seed_retry_e2e.rs","count":7},{"path":"crates/spt/tests/live_firsthost_e2e.rs","count":3},{"path":"crates/spt/tests/live_resolve_e2e.rs","count":1},{"path":"crates/spt/tests/livehost_bootgate_e2e.rs","count":1},{"path":"crates/spt/tests/ready_resume_ledger_e2e.rs","count":2},{"path":"crates/spt-daemon/tests/daemon_e2e.rs","count":1},{"path":"crates/spt-daemon/tests/digest.rs","count":3},{"path":"crates/spt-daemon/tests/dispatch.rs","count":6},{"path":"crates/spt-daemon/tests/inject_control_wedge.rs","count":1},{"path":"crates/spt-daemon/tests/pump.rs","count":1},{"path":"crates/spt-daemon/tests/replicate.rs","count":2},{"path":"crates/spt-daemon/tests/sync.rs","count":2},{"path":"crates/spt/src/rc.rs","count":11},{"path":"crates/spt-daemon/src/inject.rs","count":9}],"truncated":false,"displayContent":"# crates/\n\n## spt/\n\n### tests/\n#### active_only_never_relay_e2e.rs\n 84│            controller_node: None,\n*85│            harness_only: false,\n 86│            adapter: None,\n 87│            recent_projects: Vec::new(),\n 88│            controlled: false,\n#### contract_e2e.rs\n   8│//!\n  *9│//! A second test exercises the **harness-hosted startup** topology proper —\n *10│//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered\n  11│//! message — the REQ-START-2 path.\n  12│//\n  13│// [int->REQ-API-1]\n    │...\n 180│\n*181│/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed\n 182│/// (matched by parent pid, validated against pid-recycle), binds the perch\n 183│/// skeleton→live, and relays a delivered message.\n 184│#[test]\n    │...\n 233│        .output()\n*234│        .expect(\"run api listen\");\n 235│    assert!(\n 236│        listen.status.success(),\n*237│        \"api listen exit: {:?}\",\n 238│        listen.status\n 239│    );\n 240│    let stdout = String::from_utf8_lossy(&listen.stdout);\n    │...\n 367│        .output()\n*368│        .expect(\"run api listen\");\n 369│    let listen_err = String::from_utf8_lossy(&listen.stderr);\n 370│    assert!(\n 371│        listen.status.success(),\n*372│        \"api listen exit: {:?}\\n{listen_err}\",\n 373│        listen.status\n 374│    );\n 375│\n    │...\n 472│/// `spt api seed` transparently spins up `spt daemon`, the seed lands in its\n*473│/// in-memory map (no file), and a follow-up `api listen` TAKEs it and binds —\n 474│/// proving the cross-invocation handoff rides the daemon, not a seed file.\n 475│#[test]\n 476│fn cold_api_call_autostarts_daemon_and_handoff_is_in_memory() {\n    │...\n 534│        .output()\n*535│        .expect(\"run api listen\");\n 536│    let listen_ok = listen.status.success();\n 537│    let stdout = String::from_utf8_lossy(&listen.stdout).into_owned();\n 538│    let rec = spt_store::info::read_info(&perch_path);\n    │...\n 546│\n*547│    assert!(listen_ok, \"api listen exit: {:?}\", listen.status);\n 548│    assert!(\n 549│        stdout.contains(\"delivered after cold start\"),\n 550│        \"listen must relay the spooled message via the in-memory seed handoff, got: {stdout:?}\"\n#### dummy_harness_e2e.rs\n 245│    // attach-CONNECTS proof (Wave 1 scope): the rc pump spawned its reader and did\n*246│    // NOT RC_FAIL (no \"no live session\" / loopback-dial refusal).\n 247│    let rc_connected = rc_stderr.contains(\"PUMP_IPC_READER\") && !rc_stderr.contains(\"RC_FAIL\");\n 248│    eprintln!(\n 249│        \"=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \\\n    │...\n 329│// offline status-gate (\"offline — nothing to attach\", REQ-HAZARD-RC-ATTACH-FAILFAST\n*330│// in rc.rs run_attach) or hits \"no live session for endpoint\" (printed as\n 331│// RC_FAIL) — never connecting to the live PTY, so NO heartbeat is ever seen.\n 332│//\n 333│// THE CLAIM: the attach-by-default path (no --start) must DELIVER the live\n    │...\n 465│    // (4c) Drive the endpoint OFFLINE: kill the first harness incarnation. Its broker\n*466│    //      session then drains and the next reconcile sweep sees no live session for a\n 467│    //      dead, online-latched, controllable perch → marks it offline.\n 468│    if let Some(p) = harness_pid_1 {\n 469│        kill_pid(p);\n    │...\n 538│    let bailed_no_session =\n*539│        run_captured.contains(\"no live session\") || run_stderr_txt.contains(\"no live session\");\n 540│\n 541│    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();\n 542│    eprintln!(\n#### idle_edge_drain_e2e.rs\n  6│//! DEFERRED (window=`active_only`) awaiting a hook poll. An idle spt-hosted session\n *7│//! (no `api listen` relay to wake it) may never poll — so on the ACTIVE→IDLE edge\n  8│//! the daemon must DRAIN that pending spool (deferred AND non-deferred) through the\n  9│//! ONE shared spt-hosted inject leg (`try_spt_hosted_inject`), the same leg WAN\n 10│//! ingress and local `spt send` use. Pre-leg-2 the idle-transition drain\n#### list_json_liveness_parity_e2e.rs\n 81│            controller_node: None,\n*82│            harness_only: false,\n 83│            adapter: None,\n 84│            recent_projects: Vec::new(),\n 85│            controlled: false,\n#### listen_seed_retry_e2e.rs\n   5│//! 1. **Seed survives a pre-bind refusal** (REQ-LISTEN-SEED-CONSUME-AFTER-BIND,\n  *6│//!    hertz's headline): on a multi-subnet node an `api listen` with no `--subnet`\n   7│//!    refuses HOME AFTER it has already TAKEn the consume-once seed. The corrected\n   8│//!    retry on the SAME pid (adding `--subnet`) must still find the seed and BIND —\n   9│//!    never the NO_SEED dead end the field bug hit.\n    │...\n  11│//!    ephemeral seed is GONE (late go-live / post-daemon-restart) binds via\n *12│//!    `api listen <id> --session-id <sid>` — the loud SID_BIND marker path.\n  13│//!\n  14│//! Both drive the real binary with an explicit `--adapter mock` (bin-only crate:\n  15│//! the override is the deterministic no-registry path the sibling contract_e2e.rs\n    │...\n  66│// [int->REQ-LISTEN-SEED-CONSUME-AFTER-BIND] F-034 leg b: a pre-bind HOME refusal on\n *67│// a multi-subnet node must NOT burn the consume-once seed. The first `api listen`\n  68│// (no --subnet) TAKEs the seed then refuses HOME; the corrected retry on the SAME\n  69│// pid (adding --subnet) still finds the seed and BINDs — the NO_SEED dead end is\n  70│// gone. Proven through the real binary + real daemon IPC, no mock shim.\n    │...\n 107│        .output()\n*108│        .expect(\"run first api listen\");\n 109│    let refused_err = String::from_utf8_lossy(&refused.stderr);\n 110│    assert!(\n 111│        !refused.status.success(),\n    │...\n 136│        .output()\n*137│        .expect(\"run retry api listen\");\n 138│    let retry_err = String::from_utf8_lossy(&retry.stderr);\n 139│    let retry_out = String::from_utf8_lossy(&retry.stdout);\n 140│    assert!(\n    │...\n 167│// (its ephemeral SessionStart seed expired / a daemon restart dropped it) binds via\n*168│// `api listen <id> --session-id <sid>`. The seed path yields NO_SEED, the fallback\n 169│// binds from the sid (loud SID_BIND marker), and the perch carries the sid. This is\n 170│// the post-daemon-restart shape — the seed is simply absent.\n 171│#[test]\n    │...\n 196│        .output()\n*197│        .expect(\"run api listen --session-id\");\n 198│    let listen_err = String::from_utf8_lossy(&listen.stderr);\n 199│    let listen_out = String::from_utf8_lossy(&listen.stdout);\n 200│    assert!(\n#### live_firsthost_e2e.rs\n   4│//!\n  *5│//! a live-capable `spt api listen --once` BINDS the Self perch and marks it\n   6│//! `status=online` (the W0.2 first-host handoff — the Psyche no longer spawns in\n   7│//! this process) → the brain's `livehost::reconcile_once` then HOSTS the Psyche\n   8│//! lifecycle off that real online status → a graceful offline transition un-hosts\n    │...\n  34│\n *35│/// In-process seed daemon so `api seed`/`api listen` succeed without a REAL\n  36│/// detached `spt daemon` (shell_e2e's pattern).\n  37│fn start_inproc_daemon() {\n  38│    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());\n    │...\n 120│        Some(STATUS_ONLINE),\n*121│        \"a live `api listen` marks the perch online (the first-host handoff)\"\n 122│    );\n 123│\n 124│    // ── the brain reconcile hosts off that REAL online status ──\n#### live_resolve_e2e.rs\n 1│//! v0.9.0 bind-time adapter resolution int (REQ-START-5 / ADR-0021): the REAL\n*2│//! `spt api seed` (adapter-agnostic) → `spt api listen` (no `--adapter`) bringup,\n 3│//! proving the /sptc:live parity fix end-to-end:\n 4│//!\n 5│//! - the seed carries NO `--adapter`; `listen` with NO `--adapter` resolves the\n#### livehost_bootgate_e2e.rs\n 16│//! brain over a net-less node, hand-stamps exactly such a stale perch (the cold-start\n*17│//! leftover — there is deliberately NO `api listen`/`bind`, so the broker hosts no\n 18│//! session for it), and asserts BOTH: the perch is flipped OFFLINE at boot AND the\n 19│//! nested `{id}-psyche` perch NEVER appears (no phantom revival).\n 20│//!\n#### ready_resume_ledger_e2e.rs\n  *1│//! REQ-READY-AGENT-RESUME int — a harness-hosted READY endpoint (the `spt ready`\n   2│//! bind, `ReadyAgent::start_homed`) now LEDGERS a Boot session row, so an OFFLINE\n   3│//! ready perch carries resume rows and the picker's offline Resume-from-history\n   4│//! offers it (closing the gap that today only live agents enjoyed) — AND the daemon\n    │...\n 105│        .spawn()\n*106│        .expect(\"spawn `spt ready <id>` (the harness-hosted ready bind)\");\n 107│\n 108│    let perch = perch::resolve_perch_path(id, ParentHint::Infer);\n 109│    let deadline = Instant::now() + Duration::from_secs(25);\n\n### src/\n#### rc.rs\n 1127│    // ABSENT or status-less perch falls through to the broker path, which already\n*1128│    // ends cleanly (\"no live session for endpoint\") — not a hang. (is_perch_alive\n 1129│    // is NOT used here: it returns true for an absent info.json by interim parity.)\n 1130│    //\n 1131│    // ONLY `offline` short-circuits: attach gates on the broker SESSION existing,\n     │...\n 1296│            // The serve refused before anything ran (#4, REQ-RC-CROSS-NODE-ATTACH):\n*1297│            // no live session for the endpoint on the node we reached, or its gate\n 1298│            // denied us. On the cross-node leg this is the D6 stale-row case.\n 1299│            match &remote_node {\n 1300│                Some(node) => {\n     │...\n 1302│                        stdout,\n*1303│                        \"\\r\\n[no live session for '{endpoint_id}' on {node} — it may \\\n 1304│                         have ended, or the registry row is stale]\"\n 1305│                    );\n 1306│                }\n 1307│                None => {\n*1308│                    let _ = writeln!(stdout, \"\\r\\n[no live session for '{endpoint_id}']\");\n 1309│                }\n 1310│            }\n 1311│            Ok(())\n     │...\n 1351│enum EstablishFail {\n*1352│    /// No attachable target (no live session anywhere / ambiguous / node\n 1353│    /// unreachable) — the carried message is the CLEAN first-attempt exit copy;\n 1354│    /// the reconnect loop retries it inside the window instead (the node may\n 1355│    /// still be coming back).\n     │...\n 1438│                return Err(EstablishFail::NoTarget(format!(\n*1439│                    \"No live session for '{endpoint_id}' — not running on this node \\\n 1440│                     or any visible subnet.\"\n 1441│                )))\n 1442│            }\n     │...\n 1684│/// SEVERED under a live viewport (reconnect); nothing ever rendered ⇒ the serve\n*1685│/// REFUSED the attach (no live session / gate deny — the cross-node D6\n*1686│/// stale-row case), honest \"no live session\" instead of a false \"detached\"\n 1687│/// (#4, REQ-RC-CROSS-NODE-ATTACH).\n 1688│// [impl->REQ-RC-RECONNECT]\n 1689│fn serve_eof_end(rendered_any: bool) -> PumpEnd {\n     │...\n 1753│    /// The serve side finished the stream having produced NO output — i.e. it\n*1754│    /// refused the attach (no live session for the endpoint, or the access gate\n 1755│    /// denied us). On the cross-node leg (#4, REQ-RC-CROSS-NODE-ATTACH) this is\n 1756│    /// the D6 stale-row case: the registry routed us to a node that no longer has\n 1757│    /// the session. Distinct from [`Detached`](PumpEnd::Detached) (the OPERATOR\n 1758│    /// ended a live viewport) — here the REMOTE closed on us before anything ran,\n*1759│    /// so it renders an honest \"no live session\" instead of a false \"detached\".\n 1760│    // [impl->REQ-RC-CROSS-NODE-ATTACH]\n 1761│    NoLiveSession,\n 1762│}\n     │...\n 1865│    // Did the serve ever hand us a rendered Output frame? A serve-closed EOF with\n*1866│    // NOTHING rendered means the attach was REFUSED (no live session / gate deny) —\n 1867│    // the cross-node D6 stale-row case — not a live viewport the operator detached.\n 1868│    // Any live session replays its buffered ring on attach, so a real attach sets\n 1869│    // this before the serve would ever finish the stream. (#4, REQ-RC-CROSS-NODE-ATTACH)\n\n## spt-daemon/\n\n### tests/\n#### daemon_e2e.rs\n 10│//! 2. **Psyche-loop** — a [`BrainLifecycle`] (the consolidated re-host of the\n*11│//!    interim `api listen` live half, [`REQ-DAEMON-1`]) spawns the Psyche companion;\n 12│//!    the daemon marks the hosted perch `online`, so its liveness is the daemon\n 13│//!    `status`, never the summarizer pid ([`REQ-HAZARD-DAEMON-HOSTED-LIVENESS`]).\n 14│//! 3. **commune** — a commune drop lands in the watched dir and a scheduled pulse\n#### digest.rs\n   3│//!\n  *4│//! There is no broker and no PTY here: the endpoint is **harness-hosted** (a\n   5│//! Claude-Code-style per-session log is the worked example). The daemon projects\n   6│//! its digest **on demand** from the adapter's `[digest]` **extractor** (ADR-0019:\n   7│//! the digest's own seam, no longer riding the opaque `[history]` normalizer) run\n    │...\n  44│/// Register a harness adapter whose `[digest]` extractor reads `fixture` (a\n *45│/// digest-record JSONL), and establish a harness-hosted perch for `id` that\n  46│/// records the adapter — the daemon resolves the projection source from these two\n  47│/// facts alone (no broker, no live feed).\n  48│fn establish_cc_endpoint(id: &str, fixture: &std::path::Path) {\n    │...\n 120│    .expect(\"pull ok\")\n*121│        .expect(\"a digest projects for the harness-hosted endpoint\");\n 122│    assert!(version >= 1, \"the projection has a version\");\n 123│    assert_eq!(digest.turns.len(), 1, \"one user turn: {digest:?}\");\n 124│    assert_eq!(digest.turns[0].input.as_deref(), Some(\"add a file\"));\n#### dispatch.rs\n  529│// the attach landed on the right session without A ever naming a session id.\n *530│// A named endpoint with NO live session on B (the D6 stale-registry-row case)\n  531│// refuses cleanly: the stream finishes with no output, never a false attach.\n  532│#[test]\n  533│fn dispatcher_resolves_attach_by_endpoint_and_refuses_a_stale_endpoint() {\n     │...\n  618│\n *619│    // A STALE endpoint (no live session on B) refuses: clean finish, no output.\n  620│    let ghost = spt_daemon::attach::request_attach_endpoint(\n  621│        &mut a,\n  622│        conn.conn_id,\n     │...\n  646│                } if stream_id == ghost && !bytes.is_empty() => {\n *647│                    panic!(\"a stale endpoint with no live session must never serve output\");\n  648│                }\n  649│                _ => {}\n  650│            }\n     │...\n  654│        refused,\n *655│        \"a stale endpoint with no live session refuses with a clean stream finish\"\n  656│    );\n  657│    stop.store(true, Ordering::Relaxed);\n  658│}\n     │...\n  754│            controller_node: None,\n *755│            harness_only: false,\n  756│            adapter: None,\n  757│            recent_projects: Vec::new(),\n  758│            controlled: false,\n     │...\n 1077│            controller_node: None,\n*1078│            harness_only: false,\n 1079│            adapter: None,\n 1080│            recent_projects: Vec::new(),\n 1081│            controlled: false,\n#### inject_control_wedge.rs\n 2849│// a cross-node `spt send` funnels into `receive_wan`, which today tries the\n*2850│// harness-hosted TCP relay leg (`deliver_tcp`) then falls straight to SPOOL — it\n 2851│// has NO spt-hosted broker-inject leg (that leg exists ONLY in local cmd_send /\n 2852│// `drain_idle_window`). So a WAN arrival to a relay-less, spt-hosted perch with a\n 2853│// live, healthy translation binary sleeps in the spool FOREVER until some adapter\n#### pump.rs\n 356│                    controller_node: None,\n*357│                    harness_only: false,\n 358│                    adapter: None,\n 359│                    recent_projects: Vec::new(),\n 360│                    controlled: false,\n#### replicate.rs\n 127│        controller_node: None,\n*128│        harness_only: false,\n 129│        adapter: None,\n 130│        recent_projects: Vec::new(),\n 131│        controlled: false,\n    │...\n 166│            controller_node: None,\n*167│            harness_only: false,\n 168│            adapter: None,\n 169│            recent_projects: Vec::new(),\n 170│            controlled: false,\n#### sync.rs\n 437│            controller_node: None,\n*438│            harness_only: false,\n 439│            adapter: None,\n 440│            recent_projects: Vec::new(),\n 441│            controlled: false,\n    │...\n 454│            controller_node: None,\n*455│            harness_only: false,\n 456│            adapter: None,\n 457│            recent_projects: Vec::new(),\n 458│            controlled: false,\n\n### src/\n#### inject.rs\n  29│/// Whether `target` is an spt-hosted endpoint whose PTY the broker holds, with NO\n *30│/// `api listen` relay: it is online, has no resolvable TCP relay address, and is\n *31│/// broker-PTY-`controllable`. A harness-hosted endpoint with a relay\n  32│/// (`resolve_address` is `Some`) takes the normal TCP path; an offline or\n  33│/// non-controllable perch is not a PTY-inject target. Factored verbatim from\n  34│/// `cmd_send`'s predicate so the WAN ingress shares ONE definition.\n    │...\n 196│    // controllable, relay-LESS endpoint is an spt-hosted inject target; a relay\n*197│    // address (harness-hosted), a non-controllable perch, or an offline one is not.\n 198│    #[test]\n 199│    fn shared_inject_leg_detects_spt_hosted() {\n 200│        crate::test_home::with_home(|_| {\n    │...\n 219│\n*220│            // Not controllable (harness-hosted live agent) → not an inject target.\n 221│            // [unit->REQ-LIVE-AGENT-NO-INJECT-DELIVERY] F-033 leg (a): a\n*222│            // harness-hosted live agent is structurally excluded from PTY-inject by\n*223│            // this predicate — bind_from_seed stamps controllable=Some(false), and\n 224│            // an unset (None) field is equally non-controllable. Both shapes below.\n 225│            let mut rec = info::read_info(&perch_path).unwrap();\n 226│            rec.controllable = None;\n    │...\n 233│            let mut rec = info::read_info(&perch_path).unwrap();\n*234│            rec.controllable = Some(false);\n 235│            info::write_info(&perch_path, &rec).unwrap();\n 236│            info::set_status(&perch_path, STATUS_ONLINE).unwrap();\n 237│            assert!(\n 238│                !is_spt_hosted_no_relay(id, &owlery),\n*239│                \"controllable=Some(false) (bind_from_seed's harness-hosted stamp) is not an inject target\"\n 240│            );\n 241│\n 242│            // Offline → not an inject target regardless.\n    │...\n 281│            );\n*282│            // A relay address (harness-hosted) → not an inject target → not the belt's job.\n 283│            let addr: std::net::SocketAddr = \"127.0.0.1:6001\".parse().unwrap();\n 284│            spt_store::registry::register_address(id, &addr, &owlery).unwrap();\n 285│            assert!("},"isError":false,"timestamp":1784197517099}}
{"type":"message","id":"876a21ab","parentId":"6afd8e35","timestamp":"2026-07-16T10:25:23.320Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Reviewing contract_e2e seam implementation**"},{"type":"toolCall","id":"call_OWdMycs2JX6de9ht6BtN7KUD|fc_017bff223290b7ba016a58b192d9c4819a820a07ecec76b4cd","name":"read","arguments":{"i":"Reading harness-hosted messaging test","path":"crates/spt/tests/contract_e2e.rs","selector":"1-30,160-255"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":11985,"output":65,"cacheRead":208384,"cacheWrite":0,"totalTokens":220434,"reasoningTokens":17,"cost":{"input":0.059925000000000006,"output":0.0019500000000000001,"cacheRead":0.10419199999999999,"cacheWrite":0,"total":0.166067}},"stopReason":"toolUse","timestamp":1784197517140,"responseId":"resp_017bff223290b7ba016a58b18e0028819a8d868cfedf2510dd","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b1921ba0819a8048d6bb55a7393f","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLGS35IDHzgMiieJ0F4pVTRxg7_vQKrqwsYWCYdZe4GystDnH8oB5Usj9n20XkRNKl5gf8xI1UFLEuEQ5WUUS1U2Yu-pSKCvLSyu7VVgMBCdZCYzROGvgJVG01q8kVQ0fgpF_StKTq6-FxQypNGWh4jAkftFor3iiwKNoPKprhTinVILLVQuQcge1jGbWzxC660UlSaotVy0H8OFflNGr-Qr7qh5-ZT6m8_F6FUUOSjjzIOG6DEqe9gTkyjVb8BvLqia43p1v0UDU0out2Kd0jzIDwUeKDR8B_zsU7kAjr-S0FFzsmptVwZTz5dHJBeBxlz56ehRdyXr0eOQ6QnCuPDvup7q8XJYKbauE2L4VdYkwiQ9H6LH_1M0VJj0R_4QA1bsNgp4dURQJBaXlmjJm0MvMeuHynv3dyBkZUAFd1QSKd1CXOTnwXKmXBbkzb4eenYKPasGVgDtBwylVssHBwp6pkk84xq7JxWLzibpIkV5yTzTGkLm6Q-NEmCZQFRFZhicFT8CPYKDevicj13U0pkzmNaFS6u2YvptoERZ2u-TlZcCIDxWniuVBh8nRgrF9mT8dTFFBRVTV7PorjrpoY1JTbRApI5abvQQsvUyTSiyCse0P285SZLozv8LQRmz5ICild8zOTEghRqLlpl7hrxBceCo5m6-Mvl1p7LezeODZb2esaJ-KQu65e2_rnHdhTw8euoBBfda3aNozm298VTB3zQATPHpkX2EOsdoxVtzhueZliAeUePu0Q4z9jM6OcbrcCt3l1szAKYIIEtzZHDtbAoFpMrChxoyeMkdUDx9JYXemi1nPxTLSAKjhi3Xb1Txy5J6cS4ZX-5chhk9jW0NeGLEaF3akpnMagI2jqxk-TKsdasNKHjrw2THaJ1CjlpqApfksWdb5yZ1G-z9HVj0T8bjLfMI2ABft7UwxSq31hove4R4FQ0vBHbzkHKkEfhmlnIIvAKLecy2lfxxh-QspHTgePCebhDhdZ0avmTCcZo=","summary":[{"type":"summary_text","text":"**Reviewing contract_e2e seam implementation**"}]},{"id":"fc_017bff223290b7ba016a58b192d9c4819a820a07ecec76b4cd","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading harness-hosted messaging test\",\"path\":\"crates/spt/tests/contract_e2e.rs\",\"selector\":\"1-30,160-255\"}","call_id":"call_OWdMycs2JX6de9ht6BtN7KUD","name":"read"}]},"duration":6180.1209000004455,"ttft":4941.911299999803,"contextSnapshot":{"promptTokens":220369,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_OWdMycs2JX6de9ht6BtN7KUD|fc_017bff223290b7ba016a58b192d9c4819a820a07ecec76b4cd","toolName":"read","startedAt":"2026-07-16T10:25:23.320Z","args":{"path":"crates/spt/tests/contract_e2e.rs"},"intent":"Reading harness-hosted messaging test"},"id":"ce1f605f","parentId":"876a21ab","timestamp":"2026-07-16T10:25:23.320Z"}
{"type":"message","id":"05d7682a","parentId":"ce1f605f","timestamp":"2026-07-16T10:25:23.327Z","message":{"role":"toolResult","toolCallId":"call_OWdMycs2JX6de9ht6BtN7KUD|fc_017bff223290b7ba016a58b192d9c4819a820a07ecec76b4cd","toolName":"read","content":[{"type":"text","text":"//! M2a contract E2E (T9): the inbound + outbound halves of the harness contract\n//! proven end-to-end through the **generic mock adapter** — no vendor harness.\n//!\n//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`\n//! helper → the helper fires `spt api bind` (skeleton→live) then\n//! `spt api state idle` → a message is spooled to the perch → `spt api poll`\n//! drains it. Asserts the round-trip.\n//!\n//! A second test exercises the **harness-hosted startup** topology proper —\n//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered\n//! message — the REQ-START-2 path.\n//\n// [int->REQ-API-1]\n// [int->REQ-API-2]\n\nuse std::path::PathBuf;\nuse std::process::Command;\nuse std::sync::Mutex;\nuse std::time::Duration;\n\nmod common;\nuse common::CommandNoWindowExt;\n\n/// Both tests mutate the process-global `SPT_HOME` (the in-test spt-store calls\n/// and the spawned children read it). Serialize them so their homes don't race.\nstatic E2E_LOCK: Mutex<()> = Mutex::new(());\n\nuse spt_runtime::{AgentRuntime, Manifest, ManifestRuntime};\nuse spt_store::perch::{self, ParentHint};\nuse spt_store::spool;\n…\nfn mock_adapter_drives_the_full_contract() {\n…\n    assert!(poll.status.success(), \"api poll exit: {:?}\", poll.status);\n    let stdout = String::from_utf8_lossy(&poll.stdout);\n    assert!(\n        stdout.contains(\"hello over the contract\"),\n        \"api poll must deliver the spooled message, got: {stdout:?}\"\n    );\n\n    // Auth negative — a poll with no proof is refused (REQ-HAZARD-LOCAL-API-AUTH\n    // exercised through the real binary).\n    let unauth = Command::new(&spt_bin).no_window()\n        .args([\"api\", \"--adapter\", \"mock\", \"poll\", id])\n        .env(\"SPT_HOME\", home.path())\n        .env_remove(\"OWL_SESSION_ID\")\n        .output()\n        .expect(\"run unauth poll\");\n    assert!(\n        !unauth.status.success(),\n        \"poll without proof must be refused\"\n    );\n}\n\n/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed\n/// (matched by parent pid, validated against pid-recycle), binds the perch\n/// skeleton→live, and relays a delivered message.\n#[test]\nfn seed_then_listen_binds_and_relays() {\n    // [int->REQ-START-2]\n    // [int->REQ-API-2]\n    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());\n    let home = tempfile::tempdir().unwrap();\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let id = \"listen-e2e\";\n    let session_id = \"sid-listen-1\";\n    // The anchor must be a live process; the test runner itself qualifies.\n    let anchor = std::process::id().to_string();\n\n    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.\n    std::env::set_var(\"SPT_HOME\", home.path());\n    start_inproc_daemon();\n    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n    spool::spool_message_at(&perch_path, \"tester\", \"delivered via listen\").unwrap();\n\n    // Seed the startup record keyed by the anchor pid.\n    let seed = Command::new(&spt_bin).no_window()\n        .args([\n            \"api\",\n            \"--adapter\",\n            \"mock\",\n            \"seed\",\n            \"--pid\",\n            &anchor,\n            \"--session-id\",\n            session_id,\n        ])\n        .env(\"SPT_HOME\", home.path())\n        .status()\n        .expect(\"run api seed\");\n    assert!(seed.success(), \"api seed exit: {seed:?}\");\n\n    // Listen consumes the seed (explicit --parent-pid for a deterministic match),\n    // binds the perch, drains the backlog, and exits (--once).\n    let listen = Command::new(&spt_bin).no_window()\n        .args([\n            \"api\",\n            \"--adapter\",\n            \"mock\",\n            \"listen\",\n            id,\n            \"--parent-pid\",\n            &anchor,\n            \"--once\",\n        ])\n        .env(\"SPT_HOME\", home.path())\n        .output()\n        .expect(\"run api listen\");\n    assert!(\n        listen.status.success(),\n        \"api listen exit: {:?}\",\n        listen.status\n    );\n    let stdout = String::from_utf8_lossy(&listen.stdout);\n    assert!(\n        stdout.contains(\"delivered via listen\"),\n        \"listen must relay the spooled message, got: {stdout:?}\"\n    );\n\n    // The perch was bound live with the seed's session id.\n    let rec = spt_store::info::read_info(&perch_path).expect(\"perch info.json after listen\");\n    assert_eq!(rec.session_id, session_id);\n    assert_eq!(rec.state, \"live_agent\");\n}\n\n/// M2b LiveAgent lifecycle E2E (T9): prove the full live-agent lifecycle through\n/// the **generic mock adapter**, harness-agnostic:\n/// spawn a LiveAgent → its Psyche spawns as a nested perch → a commune drop is\n/// ingested + deleted by spt-core → a graceful `spt shutdown` fires the"}],"details":{"displayContent":{"text":"//! M2a contract E2E (T9): the inbound + outbound halves of the harness contract\n//! proven end-to-end through the **generic mock adapter** — no vendor harness.\n//!\n//! Flow: load the mock manifest → `ManifestRuntime` spawns the `mock-session`\n//! helper → the helper fires `spt api bind` (skeleton→live) then\n//! `spt api state idle` → a message is spooled to the perch → `spt api poll`\n//! drains it. Asserts the round-trip.\n//!\n//! A second test exercises the **harness-hosted startup** topology proper —\n//! `api seed` → `api listen` consumes the seed, binds, and relays a delivered\n//! message — the REQ-START-2 path.\n//\n// [int->REQ-API-1]\n// [int->REQ-API-2]\n\nuse std::path::PathBuf;\nuse std::process::Command;\nuse std::sync::Mutex;\nuse std::time::Duration;\n\nmod common;\nuse common::CommandNoWindowExt;\n\n/// Both tests mutate the process-global `SPT_HOME` (the in-test spt-store calls\n/// and the spawned children read it). Serialize them so their homes don't race.\nstatic E2E_LOCK: Mutex<()> = Mutex::new(());\n\nuse spt_runtime::{AgentRuntime, Manifest, ManifestRuntime};\nuse spt_store::perch::{self, ParentHint};\nuse spt_store::spool;\n…\nfn mock_adapter_drives_the_full_contract() {\n…\n    assert!(poll.status.success(), \"api poll exit: {:?}\", poll.status);\n    let stdout = String::from_utf8_lossy(&poll.stdout);\n    assert!(\n        stdout.contains(\"hello over the contract\"),\n        \"api poll must deliver the spooled message, got: {stdout:?}\"\n    );\n\n    // Auth negative — a poll with no proof is refused (REQ-HAZARD-LOCAL-API-AUTH\n    // exercised through the real binary).\n    let unauth = Command::new(&spt_bin).no_window()\n        .args([\"api\", \"--adapter\", \"mock\", \"poll\", id])\n        .env(\"SPT_HOME\", home.path())\n        .env_remove(\"OWL_SESSION_ID\")\n        .output()\n        .expect(\"run unauth poll\");\n    assert!(\n        !unauth.status.success(),\n        \"poll without proof must be refused\"\n    );\n}\n\n/// Harness-hosted startup proper: `api seed` then `api listen` consumes the seed\n/// (matched by parent pid, validated against pid-recycle), binds the perch\n/// skeleton→live, and relays a delivered message.\n#[test]\nfn seed_then_listen_binds_and_relays() {\n    // [int->REQ-START-2]\n    // [int->REQ-API-2]\n    let _serial = E2E_LOCK.lock().unwrap_or_else(|e| e.into_inner());\n    let home = tempfile::tempdir().unwrap();\n    let spt_bin = PathBuf::from(env!(\"CARGO_BIN_EXE_spt\"));\n    let id = \"listen-e2e\";\n    let session_id = \"sid-listen-1\";\n    // The anchor must be a live process; the test runner itself qualifies.\n    let anchor = std::process::id().to_string();\n\n    // Pre-spool a message (creates the perch spool dir) so listen drains it on bind.\n    std::env::set_var(\"SPT_HOME\", home.path());\n    start_inproc_daemon();\n    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n    spool::spool_message_at(&perch_path, \"tester\", \"delivered via listen\").unwrap();\n\n    // Seed the startup record keyed by the anchor pid.\n    let seed = Command::new(&spt_bin).no_window()\n        .args([\n            \"api\",\n            \"--adapter\",\n            \"mock\",\n            \"seed\",\n            \"--pid\",\n            &anchor,\n            \"--session-id\",\n            session_id,\n        ])\n        .env(\"SPT_HOME\", home.path())\n        .status()\n        .expect(\"run api seed\");\n    assert!(seed.success(), \"api seed exit: {seed:?}\");\n\n    // Listen consumes the seed (explicit --parent-pid for a deterministic match),\n    // binds the perch, drains the backlog, and exits (--once).\n    let listen = Command::new(&spt_bin).no_window()\n        .args([\n            \"api\",\n            \"--adapter\",\n            \"mock\",\n            \"listen\",\n            id,\n            \"--parent-pid\",\n            &anchor,\n            \"--once\",\n        ])\n        .env(\"SPT_HOME\", home.path())\n        .output()\n        .expect(\"run api listen\");\n    assert!(\n        listen.status.success(),\n        \"api listen exit: {:?}\",\n        listen.status\n    );\n    let stdout = String::from_utf8_lossy(&listen.stdout);\n    assert!(\n        stdout.contains(\"delivered via listen\"),\n        \"listen must relay the spooled message, got: {stdout:?}\"\n    );\n\n    // The perch was bound live with the seed's session id.\n    let rec = spt_store::info::read_info(&perch_path).expect(\"perch info.json after listen\");\n    assert_eq!(rec.session_id, session_id);\n    assert_eq!(rec.state, \"live_agent\");\n}\n\n/// M2b LiveAgent lifecycle E2E (T9): prove the full live-agent lifecycle through\n/// the **generic mock adapter**, harness-agnostic:\n/// spawn a LiveAgent → its Psyche spawns as a nested perch → a commune drop is\n/// ingested + deleted by spt-core → a graceful `spt shutdown` fires the","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,null,77,null,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\tests\\contract_e2e.rs"}}},"isError":false,"timestamp":1784197523327}}
{"type":"message","id":"d54e1736","parentId":"05d7682a","timestamp":"2026-07-16T10:25:47.166Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Designing test scenarios for message delivery**\n\n**Designing elevation-aware integration tests**\n\n**Identifying elevation impact on broker sessions**"},{"type":"thinking","thinking":"**Analyzing elevated process environment differences**\n\n**Investigating listener binding and permission issues**"},{"type":"toolCall","id":"call_9TcHX9f0j50anQ60Ky3M6Nr8|fc_017bff223290b7ba016a58b1aac71c819abc076ac8e9b6a80e","name":"read","arguments":{"i":"Reading local delivery decision","path":"crates/spt-msg/src/deliver.rs","selector":"88-230"}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":1988,"output":855,"cacheRead":219648,"cacheWrite":0,"totalTokens":222491,"reasoningTokens":814,"cost":{"input":0.009940000000000001,"output":0.02565,"cacheRead":0.10982399999999999,"cacheWrite":0,"total":0.145414}},"stopReason":"toolUse","timestamp":1784197523334,"responseId":"resp_017bff223290b7ba016a58b193d6dc819aa026e15d2f0a4205","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b1972860819a88d49fbc18a6aa8d","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLGjyMjZCfP8m1VyA_OlgyhLKL4vaWZudh7179JeIUzIagi3cYiq1822aEaHGBEf2qNPRPR_9E1_YEA618CB-4v04evn9pj-fnu4dOHuJRuTT_unCEehnJsSOhM5GzA9euU8NULqaA62Xpqhf9Ag7qGNeiwpIgGp2GlumskjOKnoeTALZsu0szrmWxfKU85bX5l-8niWaYBVbjxb7O2CtzpK2ZxngjICcPb29EjTnxHV8ycwMgnaN2BjpXnQwXlzCT4Vc-ChlhPL3Z8I-AOuSz9iQ7AP5-8ybcOjgmIuw-pwelocvdAw0mvXZIGiyG6_BXIqH_ETmV-UgIca8p-IdHubsBsUK-eTAqjKJoLBvhn_mhjijU5AGkb31xKBVJqTwJebi6uRocI2wTz70LLCp-dNGgIjJlpSI_P6TX7hJmOb-n1e80dVopGWtL8RZNUaWqjRxdjCGRkPDwRQMf9sVBxKeNV7Sj70lFfP3qn1quQyoUaK85g1b8m7nntApePpJ4UkovKaf69IhdHIaiNyEL62lUdHT-fC8i8TCbwTu-YOsDup1KmsjSGF0L4mFE_vVsixT8ant-FnumWdcI2SkGyUB2kErogcvAyG11n1IiU0wvo9ozj0bOA-i9omkJAwAcm_I0SX3tQdUFWyOP-DDm9iSBTuyn99tWbj0ikfmb-L2jGlUtpEhGJCSFzA6zco8CGwqcLKeZujHGvw2XdHdHesFLVCzTklw3eVDHEzRfBOov_EN7IRD9gWlRYbAhDDZ0Bhz_xn-a83VJuwlQElO6EuJARJ5PEpck02WK_HTNJIhDWMM-3Hi-PTaf59_HjrIhJu9iaOxuHBUnuAjfB5FEpu0I0cVA263VX9_8B7eqhpRnJlTzB9mo3TWZEicxMaqWT-wOvWDcQ8Zv1ybMLyI_HdVM1PdCQEiph_CkMdSNS0gZiDORQp9qFDPMzAatUt2V20p-od8T8qJMlPPPasq0QphKFyu7IBQRF999Yx2oNkw8ICfhM1QsWJ-dp4c6CiTweG-Q3Oju-V2T_3hAPyIWugs-TbLBhrmxX1n7FA6fm7_vE-RFBLUiNzjzX09CSZ727aQtd2MHziXxUFga99iSHg7zIW-k1rBD2OjD_kmf2dCdwn31jufO3zCxqJrSiuFMZg7AOSe4IGDEciyJT9xUbNPw5vp5uNULkidWoQmzZnqomZIrNWY3tA7_VThPOnaVL48c5dOw84Mk7D151RT_O0G36daFQLwykPZf4UXWdpxwgiwRPRvqyRRktbEXUTSRYenCz1Po5DghDoBT9fEBtOL59E8Z18cZ0365GTJzIU9skxAZ9R0-lhhIzuFLqWlpOtuISXcbaJTfb8d7xf3hX1FPOfPOH7jmcmiSY80aJ4ax0WfIdcy874kEQVrggmgcXIlBDD7kCeBQF_nmt1jfsOGl0KNfPOKjwP4VsDy1QGm7WAa6nmf7f__BFlg2gIGg4YJIYyYiSQDUTcSKlA8FJ2UUL-Jkdlh_lhxYhXNTKch13sEUPUgW5Nv874FnAeVhkzDtOzFS7MmQfVrAgINio4imT6rVCk3gxOnAPDQ08CLd6_ls8v5nQzoiDCB8BeFSABvTTeKlYV4PdME-PZku9xVVvf82Ks0X1yyFkWEkLV569f8BLAYf-wcFtO5lO7BM5MCnH0PyP9fbeuf3XINE6muyw_qoNxe38MMb8KjjSX46IUER_1BEcjrAOJWUsI-M8ZaxK3RZ2K-MC0OZZlYV8Qz1eWVCvaFHckXEgXdf7EtJuAsX4hMdziFDxOGR2772Nw4u4VJAkL5hsnI84psWFIFTpfj1AWe0BXm10X8mhDDN__ukds2nf2zV9o4Ig6jeJmoFLee3oGtFDqJZ2lJypUCzYv5pz9siWsIeJjPRPD10zLdPgXJFkJ-s3oB65IKFTONhWUMRKSpZRqDctdyHGin6FiTFixYnsR3rOnm_JkNznjQbZwVrnlAJdGKfPnt1OInEieHMfuIHLft06lW6bjbE5d__IwI4uaq5DhgJxPutA7MF1fT1C6C2abWJUl1iQk442raNuok4X3NoyC077sLmjGerQmr3SUbAMeQ2StFLhQXN-mn9ytzZ39pLriVEAR4a7QjWHj_PSzeyBy-GApJcFRLaYuQ8MZnISuds6xph9xpNtvAuLaqHxwPcoCfHMA_jE7vCAGA90u8NTJOZotXvAH6NLAQ9_HRM4HaKArH9TEKm3uoNjmHfhy7lLR18OveS0-utCFyuV6f62peTom27DVea93VLXMgb53tIIPw3Aw1y5we7H44oioRDIF1aYeP7GN0RktE8ovlktd5Kum2_7Ittez3xsFHmLws_ehNKmV3ttf3rVZpYci8dwhsRt4fH_xOVGc42badOxDl_i4hQ2HEA3o6uTb3Rh43Z4Nj2PV38QOS3dC8EGO4qu2jSBZPObLwUSYReRRQYflX4eDGILYEFhGJWwE-0dbHAHCLjk8JzjwOSNm8w7DWmQgLa4_QcCeYablXfK6Brv8RiX7ns5nWHR264E_P3hr6l2DRo3bsyvHhccECR-gxlMs0Epe8F2hMMpJS0I64QnA3pn1JkszaUysybUp1FaKPzGh4oSugTYyLmtzhJhyTMZFkFCCKMKmVztg7w4FjUwoc7CR0uxgldEyDIIctJKvDQJQ4Ing_nUw9zB2P7V0mCQQIVuw6aXnUPZX52jOIYTl_7GeeUfodH3Ldlj9FF_RNSscZlGAH3fFBCwpsOFK6Z3ffiHrp0gY-rwahUvMWdKFbefv518GfqPAZ-O2A54gb6YyIk9IJJeM7QtexhDY0IyZR_G-1ivEUo_-jo1D0bH_jstU7xNMR2bAx3Lxm4cJFmN41O90IG5IylB6ZsRph5-mdJRosGv1y1bFIm2qq5_NB3AyqmkUpQC3uinLysguiICvhQbh9nkO6XWcZRDhV_K2w1ZhjuSIGBZ2zOoqWYicsyB-BI3nzsszcnbtST4FliCXluHGClC_H0hO_6wlGKGTL33YUX2y0vMK2N2YG6PtU9EPgOtElbfe8a7i2uQ3Dm7ySMTxqK-iY-G5y1GMznzw1y6VianhnGtv97IOicCcA7x93wLQmOt6ecmRyYw3vVnklrsjrRo1EoiYNGGIfDY-F1OKCyQuGHHniln3JyTSX3KwBKKrwdyst4kQuiGxTFVkQtjg9DhaWP_4R3AriOyTa9fVB5PU3e_sHpEwo9xYsoM5NfK8sUv9NWGKsp0qP2FhryvWR_ZTDi_WpiZbAJUs_t3lN_MC1lS0gsmLDp7FGqVLP-qw6WQSm8iRuPT7ODYZkXCS6pDtqLCsDpYF9JnLCoyj0VkDa15I1X00zlE7NnnAEQZfu66CvKhx5xC42a5IkBX9hjIhgP-H0jMuN9kTcd5QAAFQrtlX2GxA7h6V1yqwMeo522CzxpFUPjivkgbee4wJNs_7x86N019FJ44bdtxFS8jnygG-gIOJ_6xsV-zRD9vXPXZXX1dTJ-55SkVT1DuncSbjd1fRIZ4h5n1bHHBL3_RZ9cY7j_wal_TzmR5uLGKo_ncTgLyioJb-n1MA0k6h5JzC28bUbOt-MkcdHr-DviFgk-4L_LQBNEIZa2nkdfthR0N3QXrif8Pe9d6-BJc-PijfHsmFZPgX4iK7fLzIl_qzuOMg-QcFD9a995JLuoecFq4dM0yZGJIx9BZNzmVoIU_jGwZdBliSCRKZVOcmDRudKR7tyjZAT4bqfsds5-nDuU4GvQGCw_5IZnzIXx0zqq6a3TPHNkV7vlzrdT4JI4oBgOxM-rECc2EnonR6-2BUV-Cx3_MRDljSPd9YkSdcpQvYa-vIT_fia2GyBl2lkHU8iEwE8faVkJ5XjXx-CwQjSK9o2RRrswbKzk5Gl26jw8Sfb49MYsDDcFv1h-vUb7ySkEFK6vuwIW70sT8pNkcxigWRlWclxdI9_-es74-X15ESE6xtnKF2MQ39uhCthfZgpH85rT7jwIdFuPVuRK2IidPYO_FkR3NOaPpJasIO-Vf9ecgP9J8WVwpbwIqD6gceW5BE4290wNeHy6AzNgOQp63142hpBq0oiLuQae718c16qyJHmDyreU7ewC9H1EYI9NoYxF3ticHgo4Yh9xziyKzzCw3bO4W7t5oWEvbndLYUjydlNw43KNImQrDdbkAXUSblP4WjtfDxr6iXJzEAkB-1icHT0GYagMU2f8DooGebV0hTbkca_cCI3SuZTHqw_75QnPSp1hNrSjkFqTcaP6XDCz3gDbVz7rfpgKALnlzKsQBOv3juHtjch8m0RHuLdKsZLMONmfbYsdyZoA==","summary":[{"type":"summary_text","text":"**Designing test scenarios for message delivery**"},{"type":"summary_text","text":"**Designing elevation-aware integration tests**"},{"type":"summary_text","text":"**Identifying elevation impact on broker sessions**"}]},{"id":"rs_017bff223290b7ba016a58b1a3ce94819aa61ac24d2efe26e9","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLGqXM1_SAPuGL1w1dnuZzcMP-gLxyh64DoryeIzBAMq0ZwXgigz0WkpnK0xgtaWlPJUquDO8Igs_x5qTBisZ-whZvSQOAE-SQppbIHFKGxdDewMMnc_L8bkQECiu8fQQ6r0QFBgAGlUAMfShbXJbyWX-e5FLugd1_k_oHoo8gO1Jj1mMS_ZKC4fJuH_Zm0gnFBXrIExRUjv2PjrahfWzuLNlwphU_XUq-gdkA8uoS54Yy3_uTe8OdYYeYeXsRrU82nBtiryDvH7NA2Jw5da1d8bl1h4KolP7qekuRIq-ZUank4urX0459ru7SfjOJC9R2PPQ5mQkLBdpzYPlVv0jBiPCHzWKEkAPMNj5J-B8YllTQzDdBlmLpKxCHRwCq3J2dDHNQPj0syRTpOaSmgqekqm0EGaLPd0WdrRpLWb4RX5FHDhqNsaj7N-oZ8YL7NZ1Zr4_wjqe3vvgVnSZYkDlcC-FRZobEOZIPeLb0OTfEUyWqoQFtbFHdAAh73KBC7D4CGl0tNdmdL4BHBnmRVrHWNfC4WLEb_2hs-kWsRnJwmb1Ld0wAumObhsOUbaE7hDw8xnqDn1M1RTpJskXmbE-iPC-IudO8H0I0H1Q41iZuNm2lmXzPVLNY71xPu3Boi7Vm3l_oQrKAulybBOEzzDU-6ZSdlcXy0slpAoicQZa5uljSVfVGjO5wioY0y4IqUuOJ8Bb8YZbFpvwE1mjDVhH96VINC3JNT2KQ1owK-lN9rmxugfMr71DHa0f3gdic7L0mOxYlo_26QodfvGntkHzJ_WaGZRUbTWMdUJ6SrFyjbvITOwedgKZ4-9sKF8X-lQvVBWyg9RMqn5UDG64m92Ln9rdVzDxdi_7t-c7mbdfEv1jH1KuWTFZPYx42pQB4kmGo7ME-2NqwyEs9gryZTNtoi9gPKQDBRUwDxjiSMLm9ueXyrqa3N52QfI4wfL8ueeSpSVOs4PNh1NpezixEdXOi4N_2vo_BZul5Go7yLtVZYNOl69NQg7Y8UoH0aaeKC7HczYeDe-v71ZfVoytrBTTCgoK8KMnbluX28inYkhU7aVw7a8ozNDX9FK7hszOSCPGUgFP2Z7Xscck16ajfkN1_nQHaPohoaZb8oyq1Zj9SjAm1Z29rqHZ85PKjB50_0L-977xROGze4OlZuUA5ZSntqqnzWc0P_k7ymSm_fxNIBSTh8mm9zwTm8O9kY82be_gUWx-7mgNCoka4YW27lYs73RXHfkK-muGu9RhNmZlmGH2pmdO3nBEr5YiCiaD-kjX3RLP2yhMRlEPj5EWPfhnOFeyjZKB_-ch2QhXbj4guuNk2cSV0naivj_AgSmoPkhJM90_Ti51IBNJmpmx0ihnWUPCK2G9KWlXngyb68TPtRThpR_p2Pms__wsZYGaskR377r6xGKOtgbdf6uYHtxaLwKIYLKftaEWrHvaQKxRg2HIGvL6BSRFt_it7yi-VqYLg_bFSgDJASM__nFDz3AcxkUxDubA2rFPrgwRH4DNZosl-uLcMOhMU6GtLidWRmQoLhm8IjuRy9x2bQArS6eIonmbnDOjll6i--stIf_PPYNTteDL7oz84iNSm04fR80NVxWda4yC7g7ODiZG3lSrpJ6HAzik6EwQhpy9XlSGKjpYFTIcDkCCZRIrUunkwUGud5U6tU6qAU7yqX6gkorkUCjl95J7_CyzmY9Dkg5TIknR8vSh0yKRtYF13kVIVzF_IRUae7oqXLY63JhnqwL657Y3VpykI286QtphPv_zPnEV9dZf2-NlUoI7AbiDM5JLRM-22IHTktHaNv3-8qkkqXqh1XSiMCJyroe2h_a4Og_Hb2uXvsQMi9d-fg5413-xGP_9d_dhK8yFgMal9d9EctIQx9x1wSPgFaXCHG72DRkfcK6QFmrSzw9FnFkGDW_xArhLJMGSOgspWNfzgGd3kFGsSJRcdk_KZ2B-YB1lj5uDRrWG1Cd6TIeMnhL2cjzKzMmUPjHS_IETnzFVlMKLedV6pAwjcAsng2EIsaufDJ0Pi8FHnEdVXRam6ulecesh196_xWtr02OgGqY1mqX7DONsBQWcyvGhODq1q0C9ppEadqfdVcwGKa0uWx6M1cjlDbGjuhNuF5O26F1sWm2U8jAs0ghMCVjziSseZWdFf9Kh26XQCdBE5cwyIrxczQ9U8UORIuPk-Zon_vlMqWkyq_JlIAGt6ahVNkvlfZZxN8GeNobtCgFeLCTefY3KH7BEjWzDotFW-511BQ0qGe9CM7F7ciGhmoGj3zCNjdyyiKOckGoDEnugLJuuEF-AoPduAlXPI_7YuQKomR9Eg-3wjIpCaKWROaYuxNJAU0QJTOzRKOcAUJp2GZnQ68qZ_VD5f64iT7s_dCxmyAFSEgZ4Lcxgpy-tSXEeDFUtkKTUeRiORRCa2EWM7obKUcAlEL8HBx-jk_T1WLwopbllyIVzgio_db_EcAMmNYH_V0h9wjnG3fmu1vWoDR65WZv4TC4NzhiO-AnqaDAOsz47kFq05pbAZWO_n3eNhC8eADFbOIM04oGlCnNsMLZNWZ3FTW2nk5V99SZkEumcNDILMGCnvHEIzZcBLiaoztlsH4SIbKXX8-uhcpkcKFJnmLFdkmW6vkQaadPRw6DWexbbGTLjSvamBpkooeXLu109g3tXD3Bxt4wSzkGmXkHOLg4CkyuVdxaN-RRm6JDNCY6GfbsXNs4cBdFyaG5G90oS3qkrdYmoZCp2O56Fya-xfM3lTHv2AF4zUI4I6BEdU_hMQVrXJmMQuQ6NTb5zt_vJnS5qQSycLX47aJvwJ0yVv0m0Qk-OhmY","summary":[{"type":"summary_text","text":"**Analyzing elevated process environment differences**"},{"type":"summary_text","text":"**Investigating listener binding and permission issues**"}]},{"id":"fc_017bff223290b7ba016a58b1aac71c819abc076ac8e9b6a80e","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading local delivery decision\",\"path\":\"crates/spt-msg/src/deliver.rs\",\"selector\":\"88-230\"}","call_id":"call_9TcHX9f0j50anQ60Ky3M6Nr8","name":"read"}]},"duration":23831.95120000001,"ttft":3798.701499999501,"contextSnapshot":{"promptTokens":221636,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_9TcHX9f0j50anQ60Ky3M6Nr8|fc_017bff223290b7ba016a58b1aac71c819abc076ac8e9b6a80e","toolName":"read","startedAt":"2026-07-16T10:25:47.166Z","args":{"path":"crates/spt-msg/src/deliver.rs"},"intent":"Reading local delivery decision"},"id":"d2226155","parentId":"d54e1736","timestamp":"2026-07-16T10:25:47.166Z"}
{"type":"message","id":"92570d20","parentId":"d2226155","timestamp":"2026-07-16T10:25:47.171Z","message":{"role":"toolResult","toolCallId":"call_9TcHX9f0j50anQ60Ky3M6Nr8|fc_017bff223290b7ba016a58b1aac71c819abc076ac8e9b6a80e","toolName":"read","content":[{"type":"text","text":"\n/// The live-TCP leg of [`deliver`], alone: resolve the address (stale-clean\n/// first) and attempt one framed TCP write. `true` only on a completed write —\n/// **TCP success is the only thing that counts as a live delivery.** No spool\n/// side effect on failure; callers that need their own fallback discipline\n/// (the WAN receive funnel's atomic op-id+spool insert, M4-D5a) branch on the\n/// return instead of re-implementing the resolve/connect/recycle-guard steps.\n// [impl->REQ-MSG-1]\n// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]\npub fn deliver_tcp(target: &str, from: &str, body: &str, owlery: &Path) -> bool {\n    if let Some(addr_str) = registry::resolve_address(target, owlery) {\n        if let Ok(addr) = addr_str.parse::<SocketAddr>() {\n            match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {\n                Ok(mut stream) => {\n                    if wire::write_message(&mut stream, from, body).is_ok() {\n                        return true;\n                    }\n                    // Write failed mid-stream — the caller falls back.\n                }\n                Err(_) => {\n                    // Connect refused/timed out. Recycled-pid guard (5.1): if the\n                    // owning pid is confirmed dead, drop the stale row. If the pid\n                    // is \"alive\" (possibly recycled to an unrelated process), the\n                    // refused connect already prevented misdelivery — the caller\n                    // falls back.\n                    if let Some(pid) = registry::lookup_pid(target, owlery) {\n                        if !proc::is_process_alive(pid) {\n                            let _ = registry::unregister_address(target, owlery);\n                        }\n                    }\n                }\n            }\n        }\n    }\n    false\n}\n\n/// Core delivery primitive: TCP-first, spool-fallback. Returns which path the\n/// message took. `from` is carried structurally on the wire/spool (empty =\n/// anonymous). Used by `send`, `ring`, and reply routing alike.\n// [impl->REQ-MSG-1]\n// [impl->REQ-HAZARD-INBOX-NO-DOUBLE]\npub fn deliver(target: &str, from: &str, body: &str, owlery: &Path) -> DeliveryPath {\n    // 1. Try a live TCP delivery. On success, return WITHOUT spooling (4.5: no\n    //    double-delivery).\n    if deliver_tcp(target, from, body, owlery) {\n        return DeliveryPath::Tcp;\n    }\n\n    // 2. Spool fallback — the poll drain delivers it when the target starts.\n    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n    let _ = spool::spool_message_at(&perch_path, from, body);\n    DeliveryPath::Spool\n}\n\n/// High-level fire-and-forget send. Validates the perch exists and the body is\n/// non-empty, delivers, and maps the delivery path to a [`SendOutcome`]. No\n/// I/O, no exit — the caller renders the outcome.\n///\n/// Reply routing is *this same call* with `from` set to the replier's id: the\n/// structural `from` the original message carried names the target, and the\n/// reply's own `from` lets the original sender reply back in turn.\n// [impl->REQ-MSG-1]\npub fn send(target: &str, from: &str, body: &str, owlery: &Path) -> SendOutcome {\n    let trimmed = body.trim();\n    if trimmed.is_empty() {\n        return SendOutcome::Empty;\n    }\n    // An offline-but-existing perch is still a valid spool target; only a\n    // wholly absent perch is NoPerch.\n    if !is_online(target) && !perch_exists(target) {\n        return SendOutcome::NoPerch;\n    }\n    match deliver(target, from, trimmed, owlery) {\n        DeliveryPath::Tcp => SendOutcome::Sent,\n        DeliveryPath::Spool => SendOutcome::Queued,\n    }\n}\n\n/// Deferred send (the hook channel): spool-only, **no TCP, no listener wake**.\n/// The row is tagged `deferred=1` so the live event-stream drain\n/// (`drain_non_deferred_at`) skips it — it surfaces only when a hook consumer\n/// drains everything (`drain_all_at` / `peek_all_at`) on its natural cadence.\n/// This is how a sender hands a message to an agent it must not interrupt.\n///\n/// Always `Queued` on a valid non-empty body to an existing perch — deferred\n/// delivery never reports `Sent` (there is no live handshake).\n// [impl->REQ-MSG-1]\npub fn send_deferred(target: &str, from: &str, body: &str) -> SendOutcome {\n    let trimmed = body.trim();\n    if trimmed.is_empty() {\n        return SendOutcome::Empty;\n    }\n    if !perch_exists(target) {\n        return SendOutcome::NoPerch;\n    }\n    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n    let _ = spool::spool_message_deferred_at(&perch_path, from, trimmed);\n    SendOutcome::Queued\n}\n\n/// Window-aware send (ADR-0028 W2). `window` is the delivery-window axis\n/// (`default` | `idle_only` | `active_only`); `ephemeral` is the persistence axis\n/// (carried on the row; evaporation wires in W3 with the channel axis).\n/// - `active_only` is the hook channel — spool-only, NO live TCP/wake (= the\n///   former `--deferred`).\n/// - `default` / `idle_only` try a live TCP handoff first (the relay IS the idle\n///   wake), then fall back to the spool TAGGED with the window so the drain\n///   surfaces pick it up correctly (default → both windows; idle_only →\n///   relay/idle-wake only, suppressed from the active hook poll).\n///\n/// The spool insert derives `deferred` from `window` at the chokepoint.\n// [impl->REQ-MSG-DELIVERY-AXES]\npub fn send_windowed(\n    target: &str,\n    from: &str,\n    body: &str,\n    owlery: &Path,\n    window: &str,\n    ephemeral: bool,\n) -> SendOutcome {\n    let trimmed = body.trim();\n    if trimmed.is_empty() {\n        return SendOutcome::Empty;\n    }\n    if !is_online(target) && !perch_exists(target) {\n        return SendOutcome::NoPerch;\n    }\n    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\n    // active_only never wakes — spool straight to the hook channel.\n    if window != spool::WINDOW_ACTIVE_ONLY && deliver_tcp(target, from, trimmed, owlery) {\n        return SendOutcome::Sent;\n    }\n    let _ = spool::spool_message_windowed_at(\n        &perch_path,\n        from,\n        trimmed,\n        window,\n        spool::CHANNEL_ANY,\n        ephemeral,\n    );\n    SendOutcome::Queued\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n…\n}\n\n[Showing lines 87-233 of 421. Use :234 to continue]"}],"details":{"truncation":{"content":"\r\n/// The live-TCP leg of [`deliver`], alone: resolve the address (stale-clean\r\n/// first) and attempt one framed TCP write. `true` only on a completed write —\r\n/// **TCP success is the only thing that counts as a live delivery.** No spool\r\n/// side effect on failure; callers that need their own fallback discipline\r\n/// (the WAN receive funnel's atomic op-id+spool insert, M4-D5a) branch on the\r\n/// return instead of re-implementing the resolve/connect/recycle-guard steps.\r\n// [impl->REQ-MSG-1]\r\n// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]\r\npub fn deliver_tcp(target: &str, from: &str, body: &str, owlery: &Path) -> bool {\r\n    if let Some(addr_str) = registry::resolve_address(target, owlery) {\r\n        if let Ok(addr) = addr_str.parse::<SocketAddr>() {\r\n            match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {\r\n                Ok(mut stream) => {\r\n                    if wire::write_message(&mut stream, from, body).is_ok() {\r\n                        return true;\r\n                    }\r\n                    // Write failed mid-stream — the caller falls back.\r\n                }\r\n                Err(_) => {\r\n                    // Connect refused/timed out. Recycled-pid guard (5.1): if the\r\n                    // owning pid is confirmed dead, drop the stale row. If the pid\r\n                    // is \"alive\" (possibly recycled to an unrelated process), the\r\n                    // refused connect already prevented misdelivery — the caller\r\n                    // falls back.\r\n                    if let Some(pid) = registry::lookup_pid(target, owlery) {\r\n                        if !proc::is_process_alive(pid) {\r\n                            let _ = registry::unregister_address(target, owlery);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n    false\r\n}\r\n\r\n/// Core delivery primitive: TCP-first, spool-fallback. Returns which path the\r\n/// message took. `from` is carried structurally on the wire/spool (empty =\r\n/// anonymous). Used by `send`, `ring`, and reply routing alike.\r\n// [impl->REQ-MSG-1]\r\n// [impl->REQ-HAZARD-INBOX-NO-DOUBLE]\r\npub fn deliver(target: &str, from: &str, body: &str, owlery: &Path) -> DeliveryPath {\r\n    // 1. Try a live TCP delivery. On success, return WITHOUT spooling (4.5: no\r\n    //    double-delivery).\r\n    if deliver_tcp(target, from, body, owlery) {\r\n        return DeliveryPath::Tcp;\r\n    }\r\n\r\n    // 2. Spool fallback — the poll drain delivers it when the target starts.\r\n    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\r\n    let _ = spool::spool_message_at(&perch_path, from, body);\r\n    DeliveryPath::Spool\r\n}\r\n\r\n/// High-level fire-and-forget send. Validates the perch exists and the body is\r\n/// non-empty, delivers, and maps the delivery path to a [`SendOutcome`]. No\r\n/// I/O, no exit — the caller renders the outcome.\r\n///\r\n/// Reply routing is *this same call* with `from` set to the replier's id: the\r\n/// structural `from` the original message carried names the target, and the\r\n/// reply's own `from` lets the original sender reply back in turn.\r\n// [impl->REQ-MSG-1]\r\npub fn send(target: &str, from: &str, body: &str, owlery: &Path) -> SendOutcome {\r\n    let trimmed = body.trim();\r\n    if trimmed.is_empty() {\r\n        return SendOutcome::Empty;\r\n    }\r\n    // An offline-but-existing perch is still a valid spool target; only a\r\n    // wholly absent perch is NoPerch.\r\n    if !is_online(target) && !perch_exists(target) {\r\n        return SendOutcome::NoPerch;\r\n    }\r\n    match deliver(target, from, trimmed, owlery) {\r\n        DeliveryPath::Tcp => SendOutcome::Sent,\r\n        DeliveryPath::Spool => SendOutcome::Queued,\r\n    }\r\n}\r\n\r\n/// Deferred send (the hook channel): spool-only, **no TCP, no listener wake**.\r\n/// The row is tagged `deferred=1` so the live event-stream drain\r\n/// (`drain_non_deferred_at`) skips it — it surfaces only when a hook consumer\r\n/// drains everything (`drain_all_at` / `peek_all_at`) on its natural cadence.\r\n/// This is how a sender hands a message to an agent it must not interrupt.\r\n///\r\n/// Always `Queued` on a valid non-empty body to an existing perch — deferred\r\n/// delivery never reports `Sent` (there is no live handshake).\r\n// [impl->REQ-MSG-1]\r\npub fn send_deferred(target: &str, from: &str, body: &str) -> SendOutcome {\r\n    let trimmed = body.trim();\r\n    if trimmed.is_empty() {\r\n        return SendOutcome::Empty;\r\n    }\r\n    if !perch_exists(target) {\r\n        return SendOutcome::NoPerch;\r\n    }\r\n    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\r\n    let _ = spool::spool_message_deferred_at(&perch_path, from, trimmed);\r\n    SendOutcome::Queued\r\n}\r\n\r\n/// Window-aware send (ADR-0028 W2). `window` is the delivery-window axis\r\n/// (`default` | `idle_only` | `active_only`); `ephemeral` is the persistence axis\r\n/// (carried on the row; evaporation wires in W3 with the channel axis).\r\n/// - `active_only` is the hook channel — spool-only, NO live TCP/wake (= the\r\n///   former `--deferred`).\r\n/// - `default` / `idle_only` try a live TCP handoff first (the relay IS the idle\r\n///   wake), then fall back to the spool TAGGED with the window so the drain\r\n///   surfaces pick it up correctly (default → both windows; idle_only →\r\n///   relay/idle-wake only, suppressed from the active hook poll).\r\n///\r\n/// The spool insert derives `deferred` from `window` at the chokepoint.\r\n// [impl->REQ-MSG-DELIVERY-AXES]\r\npub fn send_windowed(\r\n    target: &str,\r\n    from: &str,\r\n    body: &str,\r\n    owlery: &Path,\r\n    window: &str,\r\n    ephemeral: bool,\r\n) -> SendOutcome {\r\n    let trimmed = body.trim();\r\n    if trimmed.is_empty() {\r\n        return SendOutcome::Empty;\r\n    }\r\n    if !is_online(target) && !perch_exists(target) {\r\n        return SendOutcome::NoPerch;\r\n    }\r\n    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\r\n    // active_only never wakes — spool straight to the hook channel.\r\n    if window != spool::WINDOW_ACTIVE_ONLY && deliver_tcp(target, from, trimmed, owlery) {\r\n        return SendOutcome::Sent;\r\n    }\r\n    let _ = spool::spool_message_windowed_at(\r\n        &perch_path,\r\n        from,\r\n        trimmed,\r\n        window,\r\n        spool::CHANNEL_ANY,\r\n        ephemeral,\r\n    );\r\n    SendOutcome::Queued\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r","truncated":true,"truncatedBy":"lines","totalLines":335,"totalBytes":6361,"outputLines":147,"outputBytes":6361,"lastLinePartial":false,"firstLineExceedsLimit":false},"displayContent":{"text":"\r\n/// The live-TCP leg of [`deliver`], alone: resolve the address (stale-clean\r\n/// first) and attempt one framed TCP write. `true` only on a completed write —\r\n/// **TCP success is the only thing that counts as a live delivery.** No spool\r\n/// side effect on failure; callers that need their own fallback discipline\r\n/// (the WAN receive funnel's atomic op-id+spool insert, M4-D5a) branch on the\r\n/// return instead of re-implementing the resolve/connect/recycle-guard steps.\r\n// [impl->REQ-MSG-1]\r\n// [impl->REQ-HAZARD-WINDOWS-PID-RECYCLE]\r\npub fn deliver_tcp(target: &str, from: &str, body: &str, owlery: &Path) -> bool {\r\n    if let Some(addr_str) = registry::resolve_address(target, owlery) {\r\n        if let Ok(addr) = addr_str.parse::<SocketAddr>() {\r\n            match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {\r\n                Ok(mut stream) => {\r\n                    if wire::write_message(&mut stream, from, body).is_ok() {\r\n                        return true;\r\n                    }\r\n                    // Write failed mid-stream — the caller falls back.\r\n                }\r\n                Err(_) => {\r\n                    // Connect refused/timed out. Recycled-pid guard (5.1): if the\r\n                    // owning pid is confirmed dead, drop the stale row. If the pid\r\n                    // is \"alive\" (possibly recycled to an unrelated process), the\r\n                    // refused connect already prevented misdelivery — the caller\r\n                    // falls back.\r\n                    if let Some(pid) = registry::lookup_pid(target, owlery) {\r\n                        if !proc::is_process_alive(pid) {\r\n                            let _ = registry::unregister_address(target, owlery);\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n    false\r\n}\r\n\r\n/// Core delivery primitive: TCP-first, spool-fallback. Returns which path the\r\n/// message took. `from` is carried structurally on the wire/spool (empty =\r\n/// anonymous). Used by `send`, `ring`, and reply routing alike.\r\n// [impl->REQ-MSG-1]\r\n// [impl->REQ-HAZARD-INBOX-NO-DOUBLE]\r\npub fn deliver(target: &str, from: &str, body: &str, owlery: &Path) -> DeliveryPath {\r\n    // 1. Try a live TCP delivery. On success, return WITHOUT spooling (4.5: no\r\n    //    double-delivery).\r\n    if deliver_tcp(target, from, body, owlery) {\r\n        return DeliveryPath::Tcp;\r\n    }\r\n\r\n    // 2. Spool fallback — the poll drain delivers it when the target starts.\r\n    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\r\n    let _ = spool::spool_message_at(&perch_path, from, body);\r\n    DeliveryPath::Spool\r\n}\r\n\r\n/// High-level fire-and-forget send. Validates the perch exists and the body is\r\n/// non-empty, delivers, and maps the delivery path to a [`SendOutcome`]. No\r\n/// I/O, no exit — the caller renders the outcome.\r\n///\r\n/// Reply routing is *this same call* with `from` set to the replier's id: the\r\n/// structural `from` the original message carried names the target, and the\r\n/// reply's own `from` lets the original sender reply back in turn.\r\n// [impl->REQ-MSG-1]\r\npub fn send(target: &str, from: &str, body: &str, owlery: &Path) -> SendOutcome {\r\n    let trimmed = body.trim();\r\n    if trimmed.is_empty() {\r\n        return SendOutcome::Empty;\r\n    }\r\n    // An offline-but-existing perch is still a valid spool target; only a\r\n    // wholly absent perch is NoPerch.\r\n    if !is_online(target) && !perch_exists(target) {\r\n        return SendOutcome::NoPerch;\r\n    }\r\n    match deliver(target, from, trimmed, owlery) {\r\n        DeliveryPath::Tcp => SendOutcome::Sent,\r\n        DeliveryPath::Spool => SendOutcome::Queued,\r\n    }\r\n}\r\n\r\n/// Deferred send (the hook channel): spool-only, **no TCP, no listener wake**.\r\n/// The row is tagged `deferred=1` so the live event-stream drain\r\n/// (`drain_non_deferred_at`) skips it — it surfaces only when a hook consumer\r\n/// drains everything (`drain_all_at` / `peek_all_at`) on its natural cadence.\r\n/// This is how a sender hands a message to an agent it must not interrupt.\r\n///\r\n/// Always `Queued` on a valid non-empty body to an existing perch — deferred\r\n/// delivery never reports `Sent` (there is no live handshake).\r\n// [impl->REQ-MSG-1]\r\npub fn send_deferred(target: &str, from: &str, body: &str) -> SendOutcome {\r\n    let trimmed = body.trim();\r\n    if trimmed.is_empty() {\r\n        return SendOutcome::Empty;\r\n    }\r\n    if !perch_exists(target) {\r\n        return SendOutcome::NoPerch;\r\n    }\r\n    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\r\n    let _ = spool::spool_message_deferred_at(&perch_path, from, trimmed);\r\n    SendOutcome::Queued\r\n}\r\n\r\n/// Window-aware send (ADR-0028 W2). `window` is the delivery-window axis\r\n/// (`default` | `idle_only` | `active_only`); `ephemeral` is the persistence axis\r\n/// (carried on the row; evaporation wires in W3 with the channel axis).\r\n/// - `active_only` is the hook channel — spool-only, NO live TCP/wake (= the\r\n///   former `--deferred`).\r\n/// - `default` / `idle_only` try a live TCP handoff first (the relay IS the idle\r\n///   wake), then fall back to the spool TAGGED with the window so the drain\r\n///   surfaces pick it up correctly (default → both windows; idle_only →\r\n///   relay/idle-wake only, suppressed from the active hook poll).\r\n///\r\n/// The spool insert derives `deferred` from `window` at the chokepoint.\r\n// [impl->REQ-MSG-DELIVERY-AXES]\r\npub fn send_windowed(\r\n    target: &str,\r\n    from: &str,\r\n    body: &str,\r\n    owlery: &Path,\r\n    window: &str,\r\n    ephemeral: bool,\r\n) -> SendOutcome {\r\n    let trimmed = body.trim();\r\n    if trimmed.is_empty() {\r\n        return SendOutcome::Empty;\r\n    }\r\n    if !is_online(target) && !perch_exists(target) {\r\n        return SendOutcome::NoPerch;\r\n    }\r\n    let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);\r\n    // active_only never wakes — spool straight to the hook channel.\r\n    if window != spool::WINDOW_ACTIVE_ONLY && deliver_tcp(target, from, trimmed, owlery) {\r\n        return SendOutcome::Sent;\r\n    }\r\n    let _ = spool::spool_message_windowed_at(\r\n        &perch_path,\r\n        from,\r\n        trimmed,\r\n        window,\r\n        spool::CHANNEL_ANY,\r\n        ephemeral,\r\n    );\r\n    SendOutcome::Queued\r\n}\r\n\r\n#[cfg(test)]\r\nmod tests {\r\n    use super::*;\r\n…\n}","startLine":87,"lineNumbers":[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,null,420]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt-msg\\src\\deliver.rs"},"truncation":{"direction":"head","truncatedBy":"lines","totalLines":421,"totalBytes":6361,"outputLines":147,"outputBytes":6361,"shownRange":{"start":87,"end":233},"nextOffset":234}}},"isError":false,"timestamp":1784197547170}}
{"type":"message","id":"432bf1e5","parentId":"92570d20","timestamp":"2026-07-16T10:25:57.124Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting registry advertise mapping status**"},{"type":"toolCall","id":"call_jjzp6xHMIg2GhEJ9iYCop8fi|fc_017bff223290b7ba016a58b1b23cc8819a9eeb6f9ce70c62be","name":"irc","arguments":{"i":"Sending lifecycle conclusion","op":"send","to":"Main","message":"Lifecycle conclusion: core has no endpoint-elevation field/branch. Harness-hosted elevated is explicitly supported (docs/DEFERRED.md:27: “works: TCP listeners + talk-down pipes”); api listen binds 127.0.0.1 relay and stamps controllable=false. Such endpoints can receive via local registry→deliver_tcp→Relay, but cannot `spt rc` because rc resolves only broker sessions; harness-hosted owns its own terminal and has none. Spt-hosted endpoints are rc-able but unelevated by construction (KH 5.7 daemon de-elevation). I’ll terminal-yield exact chains/hypotheses/test seam.","replyTo":null,"await":false,"from":null,"timeoutMs":null,"peek":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":3385,"output":218,"cacheRead":220672,"cacheWrite":0,"totalTokens":224275,"reasoningTokens":27,"cost":{"input":0.016925000000000003,"output":0.00654,"cacheRead":0.11033599999999999,"cacheWrite":0,"total":0.133801}},"stopReason":"toolUse","timestamp":1784197547186,"responseId":"resp_017bff223290b7ba016a58b1ab7430819a8c86e0abb4b0aaba","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b1b11ff0819aa3f90b7b46f334d2","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLGyMA2_zCHacFzyeLDSFtqQnxluHqVVM_t0g3LpkiPYKg5nzDu_m6bQwCZbfqhPEc14X_OJepQjwcsT6DsDPOIzfiiHTkKkjtQ5E2IJoXsaX71ThomC_kM9GK3aI1aW2PFUZk-a4eE5aF4V2ZChtJWZ5ucvsy4inxGtYU2StWv_cZUIVM4BmOM2rYWRpqY27_cyBM2ZCmpbDZw5MRXiQE3a2HzhBGTwkbK5zJWCQLeMwMSMSF2zeoqAwd0UPNerIk5FZ-67XRgQbq_HUm7ZqM1F791tUllOzrfKBf4ln9Q7FlMELftYTYn5aBM07H9o1Ll_IA2O_oG_mH6Q-h5mXZNSomRenTONmIsPIZYTj7YAUfqyOOrF-8B4HutSyCmU3zXpMgx4cwcgtaHyWLvlDRitEtPdDp-yEqLquWiwVpL7ci0CqdrMN4QjBHx_0w3OCtmNyYtXyZVI0pjUqcdDkNFO24ma9XXD7A1GlUvMwqhob4YJhSPMgIpbGjqTEz3XVS8cw_Nuo5GGyRgzhYV03wX1v0tSUyHmrR0pLe2l-QuNQEIZXzF15VEae1tW0coArH6b1oA-hnc8ko5-tADaSUBU3TcCEmj0BAwWRQluy93ClHxu9j6M20MMGHpi0ztVkA-LG5haNm9mVe_ofh-TxvZbRQu5DtS20MgmZtkRPe4UJ6UMt0mMrV5FNJ50zx2S2GC2ubGwOazgnhI_JB5QAyfPBwKW5t6RvOPFDTmmeIfTYrWpmCoKAhfr6mpcXRjeL_GdBD9fzRz275H-snRdTG148VxNC2vvBILiqtSNwA0ZYB-OZCtqCijfgL_TP-ne1xpfXrvJjmFRmGqoDZsll3kqiX3qeujWWQXi4QdbMK6leJAYySS-ctxUv8jSl5oOgwf-ioXXSFC-tvFye4MUORjUq5VBWQaSgJmAjZiYZaJPtyBe5K4HIXmv7uPrCNxSo_dEeOebnCIZJesAn6Qwn3dG0wU8k8ji-dVr8Jh9fVTXRaqzns7L2TWFK74mPiEF4TZ_xsaoTZm41Y7HuLp5aLy4NLyBkQVWbg5VHQh2jp3rLb9x1r2gy0HEc4SWSvxVTfJDhyhhVzaUYfj9BilAgnvbRg==","summary":[{"type":"summary_text","text":"**Inspecting registry advertise mapping status**"}]},{"id":"fc_017bff223290b7ba016a58b1b23cc8819a9eeb6f9ce70c62be","type":"function_call","status":"completed","arguments":"{\"i\":\"Sending lifecycle conclusion\",\"op\":\"send\",\"to\":\"Main\",\"message\":\"Lifecycle conclusion: core has no endpoint-elevation field/branch. Harness-hosted elevated is explicitly supported (docs/DEFERRED.md:27: “works: TCP listeners + talk-down pipes”); api listen binds 127.0.0.1 relay and stamps controllable=false. Such endpoints can receive via local registry→deliver_tcp→Relay, but cannot `spt rc` because rc resolves only broker sessions; harness-hosted owns its own terminal and has none. Spt-hosted endpoints are rc-able but unelevated by construction (KH 5.7 daemon de-elevation). I’ll terminal-yield exact chains/hypotheses/test seam.\" ,\"replyTo\":null,\"await\":false,\"from\":null,\"timeoutMs\":null,\"peek\":null}","call_id":"call_jjzp6xHMIg2GhEJ9iYCop8fi","name":"irc"}]},"duration":9936.186199999414,"ttft":5909.191300000064,"contextSnapshot":{"promptTokens":224057,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_jjzp6xHMIg2GhEJ9iYCop8fi|fc_017bff223290b7ba016a58b1b23cc8819a9eeb6f9ce70c62be","toolName":"irc","startedAt":"2026-07-16T10:25:57.124Z","intent":"Sending lifecycle conclusion"},"id":"a9eb0eb4","parentId":"432bf1e5","timestamp":"2026-07-16T10:25:57.124Z"}
{"type":"message","id":"14bc01b2","parentId":"a9eb0eb4","timestamp":"2026-07-16T10:25:57.124Z","message":{"role":"toolResult","toolCallId":"call_jjzp6xHMIg2GhEJ9iYCop8fi|fc_017bff223290b7ba016a58b1b23cc8819a9eeb6f9ce70c62be","toolName":"irc","content":[{"type":"text","text":"Delivered to 1 peer(s):\n- Main: injected"}],"details":{"op":"send","from":"ElevatedMessaging","to":"Main","receipts":[{"to":"Main","outcome":"injected"}]},"isError":false,"timestamp":1784197557124}}
{"type":"message","id":"4e7c5b01","parentId":"14bc01b2","timestamp":"2026-07-16T10:26:06.581Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Inspecting API poll/drain contract**"},{"type":"toolCall","id":"call_BhDZQZvzn2dhvaxHSt3ncCub|fc_017bff223290b7ba016a58b1be6a0c819aafd76c65c1c1d945","name":"grep","arguments":{"i":"Locating poll drain symbols","pattern":"pub fn poll_drain|fn poll_drain|drain_non_deferred|drain_all_audited|cmd_poll","path":"crates/spt/src/api/delivery.rs; crates/spt/src/api/mod.rs","selector":null,"case":true,"gitignore":true,"skip":null}},{"type":"toolCall","id":"call_BL4tZdIMxnXT9Fjxr6LbLwZm|fc_017bff223290b7ba016a58b1be6a1c819a9f0301dfab318b73","name":"read","arguments":{"i":"Reading poll receive path","path":"crates/spt/src/api/delivery.rs","selector":"1-165,225-280"}},{"type":"toolCall","id":"call_HNeZcSkalLWmRwGcFQNlrF2w|fc_017bff223290b7ba016a58b1be6a24819a9d085cdcea991895","name":"grep","arguments":{"i":"Finding listener ownership invariants","pattern":"LISTEN_ORPHAN|SOFT-END-PRESERVES-LIVE-LISTENER|listener.*survive|registry.*listener","path":"docs/KNOWN-HAZARDS.md; crates/spt/src/api/reporting.rs; crates/spt/src/api/startup.rs","selector":null,"case":false,"gitignore":true,"skip":null}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":552,"output":289,"cacheRead":223744,"cacheWrite":0,"totalTokens":224585,"reasoningTokens":51,"cost":{"input":0.0027600000000000003,"output":0.00867,"cacheRead":0.111872,"cacheWrite":0,"total":0.123302}},"stopReason":"toolUse","timestamp":1784197557136,"responseId":"resp_017bff223290b7ba016a58b1b579e4819aac5fc7b2910803f3","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b1b929a4819a8049a0130278287b","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLG-s8Y2EcuUh6aSUtTOFy5WhYlYK91JgSuHZRmQq3e87CDeCB_M_xJ3ppMAZi3qiPiB5JHet45199z_2705UYr8RZd5--o_18sWezAXpk6CuIfIWQ2RUDlSuriDYhj-X3pNLvKomzqjOEBVzCwNgZNOH3rlX4mF7fI0ThwHi1Q3pYASf75RaGMFFgCpeBoukcYtaoQZ1X37o2HhnGwYCO6iG4zyg3EnrEdvYUbVkFoKbU0MNQcEw1q7rWkO8tgI3lUY4si3D94qa9IEitdVPe3DRJ-LNKxKA6X_ereHNzQ4q-V7QV0t98HIvo1s7LR5vW_ixAvXiZGuvMxbinwQPLMorsZ_FOKLWwKt6O5kFNv7QGuy3qukPbPOEmfqlzLkh097qd3SBF6BVQVBhwe1crwvCdpummajgUgYwBic2lOw50in8Xy-7J1gomT2yvvnCEExXTkP100iYWUxCecXgaiVvoCH_R195rW91-Un67ezOzamh070Tcj-lAHAZsIGcKNtczf24zjUhG9YmXCKZDubotZLc0V9SdUNsHnwwfF8h2Tavc70jrXtKyra3qeWdXy-XII4J55FRDhRZpymRjyFed_RjtIkMhluxlJnnkfGryeTntpr6rhABO8L2pXuaZant8KG7iVT-NOHKPsgDtVMWg-a0qYj65QTga2G64QQMO39tamgOFla7pd8koglvScw-xqhdCgObHZCUm0k0vFwy-dO0gACj0uC4zjKET1aIfNtGqm5PDYFrlAsdDKeABW_DwfMlmkFuts7fkW-Y_0NcfdzZTffn6Fes8GYupAdwMebD0sinJTlIPJaUGb-Vd9AgHSZOkTQ4otzt58WSE5oy12GV0hdy9HHGDtD1hvgZRvjzhzBfzXKHfom-Oui27UXwyG5v2gAK-8YbVwOn7mQiaY6sZy-7zLW2IfJdQ35QU7ib2m-3-iz-sjtgCNzyhzp0gvAePrcWQJ6_-ClFaz6A-BEyrT95cH6OQDKqDJ8_h3QMGtXv2I8yt9nDjLY7GE16hkk-1aokc5nJIDdvf0Ku7MKxGu5Gya-BhWfe9vvrZE65oCkLeG19q8maEIwc3RoeUbyWYIoR0QWvksLfLXtx-FwvPrmS0WGBqNlKhgGZhr5xUc6_K2Rl7ZTUDnjDgzuE7fzKbXj9RC7-XKHIHvQXDt4eg6UpBI1rCEjrXQRBCXRMbxvzTv4Au2NWl3VKblR","summary":[{"type":"summary_text","text":"**Inspecting API poll/drain contract**"}]},{"id":"fc_017bff223290b7ba016a58b1be6a0c819aafd76c65c1c1d945","type":"function_call","status":"completed","arguments":"{\"i\":\"Locating poll drain symbols\",\"pattern\":\"pub fn poll_drain|fn poll_drain|drain_non_deferred|drain_all_audited|cmd_poll\",\"path\":\"crates/spt/src/api/delivery.rs; crates/spt/src/api/mod.rs\",\"selector\":null,\"case\":true,\"gitignore\":true,\"skip\":null}","call_id":"call_BhDZQZvzn2dhvaxHSt3ncCub","name":"grep"},{"id":"fc_017bff223290b7ba016a58b1be6a1c819a9f0301dfab318b73","type":"function_call","status":"completed","arguments":"{\"i\":\"Reading poll receive path\",\"path\":\"crates/spt/src/api/delivery.rs\",\"selector\":\"1-165,225-280\"}","call_id":"call_BL4tZdIMxnXT9Fjxr6LbLwZm","name":"read"},{"id":"fc_017bff223290b7ba016a58b1be6a24819a9d085cdcea991895","type":"function_call","status":"completed","arguments":"{\"i\":\"Finding listener ownership invariants\",\"pattern\":\"LISTEN_ORPHAN|SOFT-END-PRESERVES-LIVE-LISTENER|listener.*survive|registry.*listener\",\"path\":\"docs/KNOWN-HAZARDS.md; crates/spt/src/api/reporting.rs; crates/spt/src/api/startup.rs\",\"selector\":null,\"case\":false,\"gitignore\":true,\"skip\":null}","call_id":"call_HNeZcSkalLWmRwGcFQNlrF2w","name":"grep"}]},"duration":9444.24179999996,"ttft":6858.489700000733,"contextSnapshot":{"promptTokens":224296,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BhDZQZvzn2dhvaxHSt3ncCub|fc_017bff223290b7ba016a58b1be6a0c819aafd76c65c1c1d945","toolName":"grep","startedAt":"2026-07-16T10:26:06.581Z","args":{"path":"crates/spt/src/api/delivery.rs; crates/spt/src/api/mod.rs"},"intent":"Locating poll drain symbols"},"id":"948c7b60","parentId":"4e7c5b01","timestamp":"2026-07-16T10:26:06.581Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_BL4tZdIMxnXT9Fjxr6LbLwZm|fc_017bff223290b7ba016a58b1be6a1c819a9f0301dfab318b73","toolName":"read","startedAt":"2026-07-16T10:26:06.582Z","args":{"path":"crates/spt/src/api/delivery.rs"},"intent":"Reading poll receive path"},"id":"321d701b","parentId":"948c7b60","timestamp":"2026-07-16T10:26:06.582Z"}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_HNeZcSkalLWmRwGcFQNlrF2w|fc_017bff223290b7ba016a58b1be6a24819a9d085cdcea991895","toolName":"grep","startedAt":"2026-07-16T10:26:06.582Z","args":{"path":"docs/KNOWN-HAZARDS.md; crates/spt/src/api/reporting.rs; crates/spt/src/api/startup.rs"},"intent":"Finding listener ownership invariants"},"id":"a3569fa8","parentId":"321d701b","timestamp":"2026-07-16T10:26:06.582Z"}
{"type":"message","id":"1bda4004","parentId":"a3569fa8","timestamp":"2026-07-16T10:26:06.591Z","message":{"role":"toolResult","toolCallId":"call_BhDZQZvzn2dhvaxHSt3ncCub|fc_017bff223290b7ba016a58b1be6a0c819aafd76c65c1c1d945","toolName":"grep","content":[{"type":"text","text":"# crates/spt/src/api/\n## delivery.rs\n 101|\n*102|/// Whether the idle sentinel is set (consumed by `cmd_poll`'s inject selection).\n 103|pub fn is_idle(id: &str) -> bool {\n 104|    sentinel_path(id, IDLE_SENTINEL).exists()\n 105|}\n...\n 150|// [impl->REQ-MSG-DELIVERY-AXES]\n*151|pub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {\n 152|    // Honor the manifest `[inject]` idle/activity method: only drain via the\n 153|    // hook channel when Hook is in the resolved method set. An adapter declaring\n 154|    // `idle = [\"relay\"]` never expects hook-poll to drain its idle messages —\n...\n 186|// [impl->REQ-SHELL-1]\n*187|pub fn cmd_poll_shell(id: &str, link: &str) -> i32 {\n 188|    let owlery = perch::owlery_dir();\n 189|    let Some((owner, shell_id)) = spt_daemon::shellhost::find_shell_by_token(&owlery, link) else {\n 190|        eprintln!(\"AUTH_REFUSED:{id}: no instance holds this link token\");\n...\n 199|    let audit = spool::TakerAudit::new(spool::TakerLeg::HookPoll, None, Some(std::process::id()));\n*200|    for m in spool::drain_non_deferred_audited_at(&perch_path, &audit).unwrap_or_default() {\n 201|        // RAW passthrough — the stamped frame is the whole anonymous body; it is\n 202|        // NOT wrapped in <EVENT> (see the scope carve-out above).\n 203|        println!(\"{}\", m.body);\n...\n 208|/// Drain the spool for `id`, returning the structural `(from, body)` rows\n*209|/// (oldest first). Separated from `cmd_poll` so it is unit-testable without\n 210|/// capturing stdout.\n*211|pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<spool::DrainedMsg> {\n 212|    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 213|    // The resting gate (REQ-INST-6): while the owner's durable rest record\n 214|    // reads dormant/suspended, deferred rows hold — the hook drain narrows to\n...\n 328|    #[test] // [unit->REQ-API-2] poll drains spooled messages; deferred only with the flag.\n*329|    fn poll_drains_non_deferred_by_default() {\n 330|        let _h = isolated_home();\n 331|        establish(\"alice\");\n 332|        let perch_path = perch::resolve_perch_path(\"alice\", ParentHint::Infer);\n...\n 352|    #[test]\n*353|    fn poll_drain_holds_deferred_while_resting_and_releases_once_on_wake() {\n 354|        use spt_daemon::resting::{write_rest, RestState};\n 355|        let _h = isolated_home();\n 356|        establish(\"alice\");\n...\n 425|\n*426|    // [unit->REQ-MSG-DELIVERY-AXES] cmd_poll only drains when Hook is in the resolved method set.\n 427|    #[test]\n 428|    fn poll_only_drains_when_hook_method_present() {\n 429|        let _h = isolated_home();\n...\n 444|        // Manifest with relay-only idle method → Hook NOT in set → poll_drain is not\n*445|        // called via cmd_poll (tested via the drain helper directly as a proxy).\n 446|        // Verify the method resolution: a relay-only config does not contain Hook.\n 447|        let m = Manifest::from_toml_str(\n 448|            \"[adapter]\\nname=\\\"m\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n[inject]\\nidle=[\\\"relay\\\"]\\n\",\n...\n 494|    // idle_only rows — they reach the receiver only via the idle/wake relay surface\n*495|    // (`drain_non_deferred_at`, unchanged). default → drains on the active hook;\n 496|    // active_only → only with include_deferred; idle_only → never on the hook, yes on\n 497|    // the relay.\n 498|    #[test]\n*499|    fn poll_drain_suppresses_idle_only_which_reaches_the_relay() {\n 500|        let _h = isolated_home();\n 501|        establish(\"dave\");\n 502|        let perch_path = perch::resolve_perch_path(\"dave\", ParentHint::Infer);\n...\n 525|        // The idle/wake relay surface still carries the idle_only row (deferred=0).\n*526|        let relay = spool::drain_non_deferred_at(&perch_path).unwrap();\n 527|        assert!(\n 528|            relay.iter().any(|m| m.body == \"I\"),\n 529|            \"idle_only reaches the receiver via the idle/wake relay surface\"\n## mod.rs\n 452|            // [impl->REQ-SHELL-1]\n*453|            Some(token) => delivery::cmd_poll_shell(&id, &token),\n 454|            None => gated(&id, &auth, |id| {\n*455|                delivery::cmd_poll(id, include_deferred, ctx.manifest.as_ref())\n 456|            }),\n 457|        },\n 458|        // Worker perches (T6). worker-start authenticates against the PARENT (parent"}],"details":{"scopePath":"crates/spt/src/api/delivery.rs, crates/spt/src/api/mod.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\api","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":15,"fileCount":2,"files":["crates/spt/src/api/delivery.rs","crates/spt/src/api/mod.rs"],"fileMatches":[{"path":"crates/spt/src/api/delivery.rs","count":13},{"path":"crates/spt/src/api/mod.rs","count":2}],"truncated":false,"displayContent":"# crates/spt/src/api/\n## delivery.rs\n 101│\n*102│/// Whether the idle sentinel is set (consumed by `cmd_poll`'s inject selection).\n 103│pub fn is_idle(id: &str) -> bool {\n 104│    sentinel_path(id, IDLE_SENTINEL).exists()\n 105│}\n    │...\n 150│// [impl->REQ-MSG-DELIVERY-AXES]\n*151│pub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {\n 152│    // Honor the manifest `[inject]` idle/activity method: only drain via the\n 153│    // hook channel when Hook is in the resolved method set. An adapter declaring\n 154│    // `idle = [\"relay\"]` never expects hook-poll to drain its idle messages —\n    │...\n 186│// [impl->REQ-SHELL-1]\n*187│pub fn cmd_poll_shell(id: &str, link: &str) -> i32 {\n 188│    let owlery = perch::owlery_dir();\n 189│    let Some((owner, shell_id)) = spt_daemon::shellhost::find_shell_by_token(&owlery, link) else {\n 190│        eprintln!(\"AUTH_REFUSED:{id}: no instance holds this link token\");\n    │...\n 199│    let audit = spool::TakerAudit::new(spool::TakerLeg::HookPoll, None, Some(std::process::id()));\n*200│    for m in spool::drain_non_deferred_audited_at(&perch_path, &audit).unwrap_or_default() {\n 201│        // RAW passthrough — the stamped frame is the whole anonymous body; it is\n 202│        // NOT wrapped in <EVENT> (see the scope carve-out above).\n 203│        println!(\"{}\", m.body);\n    │...\n 208│/// Drain the spool for `id`, returning the structural `(from, body)` rows\n*209│/// (oldest first). Separated from `cmd_poll` so it is unit-testable without\n 210│/// capturing stdout.\n*211│pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<spool::DrainedMsg> {\n 212│    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);\n 213│    // The resting gate (REQ-INST-6): while the owner's durable rest record\n 214│    // reads dormant/suspended, deferred rows hold — the hook drain narrows to\n    │...\n 328│    #[test] // [unit->REQ-API-2] poll drains spooled messages; deferred only with the flag.\n*329│    fn poll_drains_non_deferred_by_default() {\n 330│        let _h = isolated_home();\n 331│        establish(\"alice\");\n 332│        let perch_path = perch::resolve_perch_path(\"alice\", ParentHint::Infer);\n    │...\n 352│    #[test]\n*353│    fn poll_drain_holds_deferred_while_resting_and_releases_once_on_wake() {\n 354│        use spt_daemon::resting::{write_rest, RestState};\n 355│        let _h = isolated_home();\n 356│        establish(\"alice\");\n    │...\n 425│\n*426│    // [unit->REQ-MSG-DELIVERY-AXES] cmd_poll only drains when Hook is in the resolved method set.\n 427│    #[test]\n 428│    fn poll_only_drains_when_hook_method_present() {\n 429│        let _h = isolated_home();\n    │...\n 444│        // Manifest with relay-only idle method → Hook NOT in set → poll_drain is not\n*445│        // called via cmd_poll (tested via the drain helper directly as a proxy).\n 446│        // Verify the method resolution: a relay-only config does not contain Hook.\n 447│        let m = Manifest::from_toml_str(\n 448│            \"[adapter]\\nname=\\\"m\\\"\\nversion=\\\"1\\\"\\nmin_spt_core_version=\\\"1\\\"\\n\\n[inject]\\nidle=[\\\"relay\\\"]\\n\",\n    │...\n 494│    // idle_only rows — they reach the receiver only via the idle/wake relay surface\n*495│    // (`drain_non_deferred_at`, unchanged). default → drains on the active hook;\n 496│    // active_only → only with include_deferred; idle_only → never on the hook, yes on\n 497│    // the relay.\n 498│    #[test]\n*499│    fn poll_drain_suppresses_idle_only_which_reaches_the_relay() {\n 500│        let _h = isolated_home();\n 501│        establish(\"dave\");\n 502│        let perch_path = perch::resolve_perch_path(\"dave\", ParentHint::Infer);\n    │...\n 525│        // The idle/wake relay surface still carries the idle_only row (deferred=0).\n*526│        let relay = spool::drain_non_deferred_at(&perch_path).unwrap();\n 527│        assert!(\n 528│            relay.iter().any(|m| m.body == \"I\"),\n 529│            \"idle_only reaches the receiver via the idle/wake relay surface\"\n## mod.rs\n 452│            // [impl->REQ-SHELL-1]\n*453│            Some(token) => delivery::cmd_poll_shell(&id, &token),\n 454│            None => gated(&id, &auth, |id| {\n*455│                delivery::cmd_poll(id, include_deferred, ctx.manifest.as_ref())\n 456│            }),\n 457│        },\n 458│        // Worker perches (T6). worker-start authenticates against the PARENT (parent"},"isError":false,"timestamp":1784197566590}}
{"type":"message","id":"ee24e5e5","parentId":"1bda4004","timestamp":"2026-07-16T10:26:06.596Z","message":{"role":"toolResult","toolCallId":"call_BL4tZdIMxnXT9Fjxr6LbLwZm|fc_017bff223290b7ba016a58b1be6a1c819a9f0301dfab318b73","toolName":"read","content":[{"type":"text","text":"//! Delivery + activity: `api state`, `api echo-gate`, `api poll`, and\n//! inject-method selection.\n//!\n//! Activity state lives in the perch as **spt-core-owned sentinels** (CONTEXT\n//! §activity/idle detection) — the adapter only *calls* the commands; it never\n//! names or writes the sentinels (MANIFEST §What is NOT in the manifest):\n//! - `.idle`      — the activity/idle sentinel (`api state busy|idle`).\n//! - `.more-done` — the echo-commune gate (armed by `api state idle` unless\n//!   `--no-gate`; togglable directly via `api echo-gate set|clear`). This is the\n//!   `can_inject = false` fallback path: a hook that can't inject context arms\n//!   the gate so a later relay/poll surfaces it instead (CONTEXT §hook output).\n//!\n//! `api poll` is the **short-lived hook-channel drain** (distinct from\n//! `api listen`'s long-running relay): it drains the spool and prints each\n//! message framed to stdout. The inject *method* (hook/relay; PTY is M3) is\n//! resolved from the manifest's `[inject]` table.\n//\n// [impl->REQ-SEAM-ACTIVITY]\n// [impl->REQ-SEAM-INJECT]\n// [impl->REQ-API-2]\n\nuse spt_runtime::manifest::InjectMethod;\nuse spt_runtime::Manifest;\nuse spt_store::perch::{self, ParentHint, ECHO_GATE_SENTINEL, IDLE_SENTINEL};\nuse spt_store::spool;\n\nuse super::EXIT_REFUSED;\n\nfn sentinel_path(id: &str, name: &str) -> std::path::PathBuf {\n    perch::resolve_perch_path(id, ParentHint::Infer).join(name)\n}\n\n/// Set/clear the `.idle` sentinel for an activity transition. Idle additionally\n/// arms the echo-gate unless `no_gate` (CONTEXT §api state).\n// [impl->REQ-SEAM-ACTIVITY]\n// [impl->REQ-MSG-DELIVERY-AXES]\npub fn cmd_state(id: &str, state: &str, no_gate: bool) -> i32 {\n    let idle = sentinel_path(id, IDLE_SENTINEL);\n    match state {\n        \"idle\" => {\n            if let Err(e) = std::fs::write(&idle, \"\") {\n                eprintln!(\"STATE_FAIL:{id}: {e}\");\n                return EXIT_REFUSED;\n            }\n            if !no_gate {\n                let _ = std::fs::write(sentinel_path(id, ECHO_GATE_SENTINEL), \"\");\n            }\n            eprintln!(\n                \"IDLE:{id}{}\",\n                if no_gate {\n                    \" (no-gate)\"\n                } else {\n                    \" (gate-armed)\"\n                }\n            );\n            // Idle-transition DRAIN: try to inject any queued non-deferred messages\n            // now that the sentinel is written and the broker will route them via the\n            // translation binary. Rows that don't inject stay in the spool for\n            // relay/poll. Best-effort: errors are silent (spool is the fallback).\n            drain_idle_window(id);\n            0\n        }\n        \"busy\" => {\n            // Removing a non-existent sentinel is fine (idempotent transition).\n            let _ = std::fs::remove_file(&idle);\n            eprintln!(\"BUSY:{id}\");\n            0\n        }\n        other => {\n            eprintln!(\"USAGE: state must be busy|idle, got {other}\");\n            EXIT_REFUSED\n        }\n    }\n}\n\n/// `api echo-gate set|clear` — explicit control of the echo-commune gate,\n/// independent of idle (CONTEXT §api state).\n// [impl->REQ-SEAM-ACTIVITY]\npub fn cmd_echo_gate(id: &str, action: &str) -> i32 {\n    let gate = sentinel_path(id, ECHO_GATE_SENTINEL);\n    match action {\n        \"set\" => {\n            if let Err(e) = std::fs::write(&gate, \"\") {\n                eprintln!(\"ECHO_GATE_FAIL:{id}: {e}\");\n                return EXIT_REFUSED;\n            }\n            eprintln!(\"ECHO_GATE_SET:{id}\");\n            0\n        }\n        \"clear\" => {\n            let _ = std::fs::remove_file(&gate);\n            eprintln!(\"ECHO_GATE_CLEAR:{id}\");\n            0\n        }\n        other => {\n            eprintln!(\"USAGE: echo-gate must be set|clear, got {other}\");\n            EXIT_REFUSED\n        }\n    }\n}\n\n/// Whether the idle sentinel is set (consumed by `cmd_poll`'s inject selection).\npub fn is_idle(id: &str) -> bool {\n    sentinel_path(id, IDLE_SENTINEL).exists()\n}\n/// Whether the echo-gate is armed. A reader for the M2b echo-commune trigger;\n/// exercised by the activity tests today.\n#[allow(dead_code)] // consumed by the M2b echo-commune path\npub fn echo_gate_armed(id: &str) -> bool {\n    sentinel_path(id, ECHO_GATE_SENTINEL).exists()\n}\n\n/// Resolve the inject methods for an activity state from the manifest's\n/// `[inject]` table, **dropping `pty`** (PTY injection is M3 — `spt-term`). With\n/// no manifest or no configured methods, default to `hook` (the universally-\n/// available channel). REQ-SEAM-INJECT.\n// [impl->REQ-SEAM-INJECT]\npub fn resolve_inject_methods(manifest: Option<&Manifest>, idle: bool) -> Vec<InjectMethod> {\n    let configured =\n        manifest.and_then(|m| m.inject.as_ref()).map(\n            |inj| {\n                if idle {\n                    &inj.idle\n                } else {\n                    &inj.activity\n                }\n            },\n        );\n    let mut methods: Vec<InjectMethod> = configured\n        .map(|v| {\n            v.iter()\n                .copied()\n                .filter(|m| *m != InjectMethod::Pty) // PTY = M3\n                .collect()\n        })\n        .unwrap_or_default();\n    if methods.is_empty() {\n        methods.push(InjectMethod::Hook);\n    }\n    methods\n}\n\n/// `api poll <id>` — drain the spool (hook-channel delivery) and print each\n/// message as ONE whole `<EVENT>` envelope to stdout (ADR-0020: the canonical\n/// arriving-message format at every surface; whole, never `<EVENT-PART>`, so the\n/// adapter's hook composer needs no reassembly — self-delimiting, F-002\n/// dissolves). Non-deferred only unless `include_deferred`.\n// [impl->REQ-API-2]\n// [impl->REQ-MSG-ENVELOPE]\n// [impl->REQ-MSG-DELIVERY-AXES]\npub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {\n    // Honor the manifest `[inject]` idle/activity method: only drain via the\n    // hook channel when Hook is in the resolved method set. An adapter declaring\n    // `idle = [\"relay\"]` never expects hook-poll to drain its idle messages —\n    // relay is its live channel; draining here would double-consume the rows.\n    // Default (no manifest / empty config after PTY-filter) = [Hook] → drain as\n    // before; existing adapters are unaffected.\n    let methods = resolve_inject_methods(manifest, is_idle(id));\n    if methods.contains(&InjectMethod::Hook) {\n        for m in poll_drain(id, include_deferred) {\n            emit_event(&m.from, &m.body);\n        }\n    }\n    0\n}\n…\npub fn poll_drain(id: &str, include_deferred: bool) -> Vec<spool::DrainedMsg> {\n…\n    // [impl->REQ-SPOOL-TAKE-AUDIT] hook-poll leg — record who drained the row.\n    let audit = spool::TakerAudit::new(spool::TakerLeg::HookPoll, None, Some(std::process::id()));\n    spool::drain_active_window_audited_at(&perch_path, include_deferred, &audit).unwrap_or_default()\n}\n\n/// Idle-transition DRAIN (F-023 leg 2, REQ-MSG-IDLE-EDGE-DRAIN): on the\n/// ACTIVE→IDLE edge, drain the endpoint's pending spool — deferred AND\n/// non-deferred — through the ONE shared spt-hosted inject leg\n/// ([`spt_daemon::try_spt_hosted_inject`], the same leg WAN ingress and local\n/// `spt send` use). This closes the second F-023 starvation gap: a relay-LESS\n/// spt-hosted endpoint has no `api listen` wake, so BOTH stranded classes (the\n/// `active_only` rows a hook-poll never comes to carry, and the `default`/`idle_only`\n/// rows waiting on a relay that does not exist) would otherwise sleep forever.\n///\n/// Take/ack (idempotent under a concurrent `api poll`): [`spool::claim_idle_edge_at`]\n/// marks the taken rows `delivered = 1` in one IMMEDIATE transaction — a racing\n/// hook-poll drain then cannot re-take them (ONE spool, two triggers, no dup).\n/// A claimed row the inject leg cannot carry (no live translation binary, still\n/// ACTIVE, or a broker miss) is RELEASED back intact via [`spool::release_at`]\n/// (v0.14.3 LAW: it SPOOLS LOUD, the broker never writes the PTY raw). Best-effort:\n/// all errors are silent — the spool is always the fallback.\n// [impl->REQ-MSG-IDLE-EDGE-DRAIN]\n// [impl->REQ-HAZARD-DELIVERY-STARVATION]\n// [impl->REQ-MSG-DELIVERY-AXES]\nfn drain_idle_window(id: &str) {\n    if !spt_daemon::is_running() {\n        return;\n    }\n    // The claim/offer/release/evaporate core is the ONE shared `drain_idle_spool`\n    // (spt_daemon::inject) — the same leg the send-time already-idle path and the\n    // pulse-tick re-offer belt (REQ-IDLE-PARKED-DELIVERY) call, so the idle-EDGE drain\n    // and the parked-idle drain cannot drift.\n    let owlery = perch::owlery_dir();\n    spt_daemon::inject::drain_idle_spool(id, &owlery);\n}\n\n/// Emit one drained row to a hook-channel surface as ONE whole `<EVENT>` line\n/// (composed via [`spt_msg::emit::render_event_whole`]: typed bodies pass\n/// through verbatim, plain bodies compose `<EVENT type=\"msg\" …>`; never\n/// chunked). Self-delimiting by construction.\n// [impl->REQ-MSG-ENVELOPE]\nfn emit_event(from: &str, body: &str) {\n    use std::io::Write;\n    println!(\"{}\", spt_msg::emit::render_event_whole(from, body));\n    let _ = std::io::stdout().flush();\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::testutil::isolated_home;\n    use spt_store::info::{self, InfoJson};\n\n    fn establish(id: &str) {\n        let path = perch::resolve_perch_path(id, ParentHint::Infer);\n        std::fs::create_dir_all(&path).unwrap();\n…\n    }\n…\n}"}],"details":{"displayContent":{"text":"//! Delivery + activity: `api state`, `api echo-gate`, `api poll`, and\n//! inject-method selection.\n//!\n//! Activity state lives in the perch as **spt-core-owned sentinels** (CONTEXT\n//! §activity/idle detection) — the adapter only *calls* the commands; it never\n//! names or writes the sentinels (MANIFEST §What is NOT in the manifest):\n//! - `.idle`      — the activity/idle sentinel (`api state busy|idle`).\n//! - `.more-done` — the echo-commune gate (armed by `api state idle` unless\n//!   `--no-gate`; togglable directly via `api echo-gate set|clear`). This is the\n//!   `can_inject = false` fallback path: a hook that can't inject context arms\n//!   the gate so a later relay/poll surfaces it instead (CONTEXT §hook output).\n//!\n//! `api poll` is the **short-lived hook-channel drain** (distinct from\n//! `api listen`'s long-running relay): it drains the spool and prints each\n//! message framed to stdout. The inject *method* (hook/relay; PTY is M3) is\n//! resolved from the manifest's `[inject]` table.\n//\n// [impl->REQ-SEAM-ACTIVITY]\n// [impl->REQ-SEAM-INJECT]\n// [impl->REQ-API-2]\n\nuse spt_runtime::manifest::InjectMethod;\nuse spt_runtime::Manifest;\nuse spt_store::perch::{self, ParentHint, ECHO_GATE_SENTINEL, IDLE_SENTINEL};\nuse spt_store::spool;\n\nuse super::EXIT_REFUSED;\n\nfn sentinel_path(id: &str, name: &str) -> std::path::PathBuf {\n    perch::resolve_perch_path(id, ParentHint::Infer).join(name)\n}\n\n/// Set/clear the `.idle` sentinel for an activity transition. Idle additionally\n/// arms the echo-gate unless `no_gate` (CONTEXT §api state).\n// [impl->REQ-SEAM-ACTIVITY]\n// [impl->REQ-MSG-DELIVERY-AXES]\npub fn cmd_state(id: &str, state: &str, no_gate: bool) -> i32 {\n    let idle = sentinel_path(id, IDLE_SENTINEL);\n    match state {\n        \"idle\" => {\n            if let Err(e) = std::fs::write(&idle, \"\") {\n                eprintln!(\"STATE_FAIL:{id}: {e}\");\n                return EXIT_REFUSED;\n            }\n            if !no_gate {\n                let _ = std::fs::write(sentinel_path(id, ECHO_GATE_SENTINEL), \"\");\n            }\n            eprintln!(\n                \"IDLE:{id}{}\",\n                if no_gate {\n                    \" (no-gate)\"\n                } else {\n                    \" (gate-armed)\"\n                }\n            );\n            // Idle-transition DRAIN: try to inject any queued non-deferred messages\n            // now that the sentinel is written and the broker will route them via the\n            // translation binary. Rows that don't inject stay in the spool for\n            // relay/poll. Best-effort: errors are silent (spool is the fallback).\n            drain_idle_window(id);\n            0\n        }\n        \"busy\" => {\n            // Removing a non-existent sentinel is fine (idempotent transition).\n            let _ = std::fs::remove_file(&idle);\n            eprintln!(\"BUSY:{id}\");\n            0\n        }\n        other => {\n            eprintln!(\"USAGE: state must be busy|idle, got {other}\");\n            EXIT_REFUSED\n        }\n    }\n}\n\n/// `api echo-gate set|clear` — explicit control of the echo-commune gate,\n/// independent of idle (CONTEXT §api state).\n// [impl->REQ-SEAM-ACTIVITY]\npub fn cmd_echo_gate(id: &str, action: &str) -> i32 {\n    let gate = sentinel_path(id, ECHO_GATE_SENTINEL);\n    match action {\n        \"set\" => {\n            if let Err(e) = std::fs::write(&gate, \"\") {\n                eprintln!(\"ECHO_GATE_FAIL:{id}: {e}\");\n                return EXIT_REFUSED;\n            }\n            eprintln!(\"ECHO_GATE_SET:{id}\");\n            0\n        }\n        \"clear\" => {\n            let _ = std::fs::remove_file(&gate);\n            eprintln!(\"ECHO_GATE_CLEAR:{id}\");\n            0\n        }\n        other => {\n            eprintln!(\"USAGE: echo-gate must be set|clear, got {other}\");\n            EXIT_REFUSED\n        }\n    }\n}\n\n/// Whether the idle sentinel is set (consumed by `cmd_poll`'s inject selection).\npub fn is_idle(id: &str) -> bool {\n    sentinel_path(id, IDLE_SENTINEL).exists()\n}\n/// Whether the echo-gate is armed. A reader for the M2b echo-commune trigger;\n/// exercised by the activity tests today.\n#[allow(dead_code)] // consumed by the M2b echo-commune path\npub fn echo_gate_armed(id: &str) -> bool {\n    sentinel_path(id, ECHO_GATE_SENTINEL).exists()\n}\n\n/// Resolve the inject methods for an activity state from the manifest's\n/// `[inject]` table, **dropping `pty`** (PTY injection is M3 — `spt-term`). With\n/// no manifest or no configured methods, default to `hook` (the universally-\n/// available channel). REQ-SEAM-INJECT.\n// [impl->REQ-SEAM-INJECT]\npub fn resolve_inject_methods(manifest: Option<&Manifest>, idle: bool) -> Vec<InjectMethod> {\n    let configured =\n        manifest.and_then(|m| m.inject.as_ref()).map(\n            |inj| {\n                if idle {\n                    &inj.idle\n                } else {\n                    &inj.activity\n                }\n            },\n        );\n    let mut methods: Vec<InjectMethod> = configured\n        .map(|v| {\n            v.iter()\n                .copied()\n                .filter(|m| *m != InjectMethod::Pty) // PTY = M3\n                .collect()\n        })\n        .unwrap_or_default();\n    if methods.is_empty() {\n        methods.push(InjectMethod::Hook);\n    }\n    methods\n}\n\n/// `api poll <id>` — drain the spool (hook-channel delivery) and print each\n/// message as ONE whole `<EVENT>` envelope to stdout (ADR-0020: the canonical\n/// arriving-message format at every surface; whole, never `<EVENT-PART>`, so the\n/// adapter's hook composer needs no reassembly — self-delimiting, F-002\n/// dissolves). Non-deferred only unless `include_deferred`.\n// [impl->REQ-API-2]\n// [impl->REQ-MSG-ENVELOPE]\n// [impl->REQ-MSG-DELIVERY-AXES]\npub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {\n    // Honor the manifest `[inject]` idle/activity method: only drain via the\n    // hook channel when Hook is in the resolved method set. An adapter declaring\n    // `idle = [\"relay\"]` never expects hook-poll to drain its idle messages —\n    // relay is its live channel; draining here would double-consume the rows.\n    // Default (no manifest / empty config after PTY-filter) = [Hook] → drain as\n    // before; existing adapters are unaffected.\n    let methods = resolve_inject_methods(manifest, is_idle(id));\n    if methods.contains(&InjectMethod::Hook) {\n        for m in poll_drain(id, include_deferred) {\n            emit_event(&m.from, &m.body);\n        }\n    }\n    0\n}\n…\npub fn poll_drain(id: &str, include_deferred: bool) -> Vec<spool::DrainedMsg> {\n…\n    // [impl->REQ-SPOOL-TAKE-AUDIT] hook-poll leg — record who drained the row.\n    let audit = spool::TakerAudit::new(spool::TakerLeg::HookPoll, None, Some(std::process::id()));\n    spool::drain_active_window_audited_at(&perch_path, include_deferred, &audit).unwrap_or_default()\n}\n\n/// Idle-transition DRAIN (F-023 leg 2, REQ-MSG-IDLE-EDGE-DRAIN): on the\n/// ACTIVE→IDLE edge, drain the endpoint's pending spool — deferred AND\n/// non-deferred — through the ONE shared spt-hosted inject leg\n/// ([`spt_daemon::try_spt_hosted_inject`], the same leg WAN ingress and local\n/// `spt send` use). This closes the second F-023 starvation gap: a relay-LESS\n/// spt-hosted endpoint has no `api listen` wake, so BOTH stranded classes (the\n/// `active_only` rows a hook-poll never comes to carry, and the `default`/`idle_only`\n/// rows waiting on a relay that does not exist) would otherwise sleep forever.\n///\n/// Take/ack (idempotent under a concurrent `api poll`): [`spool::claim_idle_edge_at`]\n/// marks the taken rows `delivered = 1` in one IMMEDIATE transaction — a racing\n/// hook-poll drain then cannot re-take them (ONE spool, two triggers, no dup).\n/// A claimed row the inject leg cannot carry (no live translation binary, still\n/// ACTIVE, or a broker miss) is RELEASED back intact via [`spool::release_at`]\n/// (v0.14.3 LAW: it SPOOLS LOUD, the broker never writes the PTY raw). Best-effort:\n/// all errors are silent — the spool is always the fallback.\n// [impl->REQ-MSG-IDLE-EDGE-DRAIN]\n// [impl->REQ-HAZARD-DELIVERY-STARVATION]\n// [impl->REQ-MSG-DELIVERY-AXES]\nfn drain_idle_window(id: &str) {\n    if !spt_daemon::is_running() {\n        return;\n    }\n    // The claim/offer/release/evaporate core is the ONE shared `drain_idle_spool`\n    // (spt_daemon::inject) — the same leg the send-time already-idle path and the\n    // pulse-tick re-offer belt (REQ-IDLE-PARKED-DELIVERY) call, so the idle-EDGE drain\n    // and the parked-idle drain cannot drift.\n    let owlery = perch::owlery_dir();\n    spt_daemon::inject::drain_idle_spool(id, &owlery);\n}\n\n/// Emit one drained row to a hook-channel surface as ONE whole `<EVENT>` line\n/// (composed via [`spt_msg::emit::render_event_whole`]: typed bodies pass\n/// through verbatim, plain bodies compose `<EVENT type=\"msg\" …>`; never\n/// chunked). Self-delimiting by construction.\n// [impl->REQ-MSG-ENVELOPE]\nfn emit_event(from: &str, body: &str) {\n    use std::io::Write;\n    println!(\"{}\", spt_msg::emit::render_event_whole(from, body));\n    let _ = std::io::stdout().flush();\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use crate::testutil::isolated_home;\n    use spt_store::info::{self, InfoJson};\n\n    fn establish(id: &str) {\n        let path = perch::resolve_perch_path(id, ParentHint::Infer);\n        std::fs::create_dir_all(&path).unwrap();\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,null,211,null,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,null,286,null,544]},"meta":{"source":{"type":"path","value":"C:\\Users\\decid\\Documents\\projects\\spt-core\\crates\\spt\\src\\api\\delivery.rs"}}},"isError":false,"timestamp":1784197566596}}
{"type":"message","id":"b3aac80e","parentId":"ee24e5e5","timestamp":"2026-07-16T10:26:06.645Z","message":{"role":"toolResult","toolCallId":"call_HNeZcSkalLWmRwGcFQNlrF2w|fc_017bff223290b7ba016a58b1be6a24819a9d085cdcea991895","toolName":"grep","content":[{"type":"text","text":"# crates/spt/src/api/\n## reporting.rs\n 254|        let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n*255|        // F-2 (REQ-SOFT-END-PRESERVES-LIVE-LISTENER): the relay address row is\n*256|        // LISTENER-scoped truth, not session-scoped — the poll listener SURVIVES\n 257|        // a /clear (whose adapter hook fires this soft end for the DEPARTING\n 258|        // session), and only the listener process knows its socket addr, so\n 259|        // deleting a live owner's row severs delivery unrecoverably (post-clear\n...\n 263|        // the delivery failed-dial sweep (REQ-HAZARD-REGISTRY-STALE-CLEAN).\n*264|        // [impl->REQ-SOFT-END-PRESERVES-LIVE-LISTENER]\n 265|        let row_alive = registry::lookup_pid(id, &owlery)\n 266|            .map(|pid| spt_store::liveness::is_registry_entry_alive(id, pid, &owlery))\n 267|            .unwrap_or(false);\n...\n 1243|\n*1244|    // [unit->REQ-SOFT-END-PRESERVES-LIVE-LISTENER] F-2 red-first: the relay row is\n 1245|    // LISTENER-scoped truth — a soft session-end (the /clear departing-session hook)\n 1246|    // must NOT delete a row whose owner is still alive (the surviving poll listener),\n 1247|    // or every post-clear send misses the lookup → NO_PERCH while ready reads healthy.\n...\n 1265|\n*1266|    // [unit->REQ-SOFT-END-PRESERVES-LIVE-LISTENER] the dead-owner control: today's\n 1267|    // cleanup is kept — a row whose registered pid is dead is removed at soft end.\n 1268|    #[test]\n 1269|    fn soft_end_still_cleans_a_dead_listeners_address() {\n...\n 1284|\n*1285|    // [unit->REQ-SOFT-END-PRESERVES-LIVE-LISTENER] erase stays unconditional: the\n 1286|    // hard wipe orphans any listener by intent, so its row dies with the endpoint.\n 1287|    #[test]\n 1288|    fn erase_unregisters_regardless_of_liveness() {\n## startup.rs\n 505|// [impl->REQ-HAZARD-LISTEN-ORPHAN]\n*506|fn listen_orphan_exit_line(id: &str, parent_pid: u32) -> String {\n 507|    format!(\n*508|        \"LISTEN_ORPHAN_EXIT:{id}: parent pid {parent_pid} is gone — exiting the orphaned \\\n 509|         listener so the perch flips OFFLINE (its recorded pid dies) and dead-owner rebind \\\n 510|         unblocks [REQ-HAZARD-LISTEN-ORPHAN]\"\n 511|    )\n...\n 527|        if parent_is_gone(parent_pid) {\n*528|            eprintln!(\"{}\", listen_orphan_exit_line(&id, parent_pid));\n 529|            std::process::exit(EXIT_PARENT_GONE);\n 530|        }\n 531|    });\n...\n 870|    #[test]\n*871|    fn listen_orphan_exit_line_names_self_and_parent() {\n*872|        let line = listen_orphan_exit_line(\"flynn\", 4242);\n*873|        assert!(line.contains(\"LISTEN_ORPHAN_EXIT\"), \"carries the tell\");\n 874|        assert!(line.contains(\"flynn\"), \"names the listener\");\n 875|        assert!(line.contains(\"4242\"), \"names the dead parent pid\");\n 876|        assert!(line.contains(\"REQ-HAZARD-LISTEN-ORPHAN\"));"}],"details":{"scopePath":"docs/KNOWN-HAZARDS.md, crates/spt/src/api/reporting.rs, crates/spt/src/api/startup.rs","searchPath":"C:\\Users\\decid\\Documents\\projects\\spt-core","cwd":"C:\\Users\\decid\\Documents\\projects\\spt-core","matchCount":12,"fileCount":2,"files":["crates/spt/src/api/reporting.rs","crates/spt/src/api/startup.rs"],"fileMatches":[{"path":"crates/spt/src/api/reporting.rs","count":6},{"path":"crates/spt/src/api/startup.rs","count":6}],"truncated":false,"displayContent":"# crates/spt/src/api/\n## reporting.rs\n  254│        let _ = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer));\n *255│        // F-2 (REQ-SOFT-END-PRESERVES-LIVE-LISTENER): the relay address row is\n *256│        // LISTENER-scoped truth, not session-scoped — the poll listener SURVIVES\n  257│        // a /clear (whose adapter hook fires this soft end for the DEPARTING\n  258│        // session), and only the listener process knows its socket addr, so\n  259│        // deleting a live owner's row severs delivery unrecoverably (post-clear\n     │...\n  263│        // the delivery failed-dial sweep (REQ-HAZARD-REGISTRY-STALE-CLEAN).\n *264│        // [impl->REQ-SOFT-END-PRESERVES-LIVE-LISTENER]\n  265│        let row_alive = registry::lookup_pid(id, &owlery)\n  266│            .map(|pid| spt_store::liveness::is_registry_entry_alive(id, pid, &owlery))\n  267│            .unwrap_or(false);\n     │...\n 1243│\n*1244│    // [unit->REQ-SOFT-END-PRESERVES-LIVE-LISTENER] F-2 red-first: the relay row is\n 1245│    // LISTENER-scoped truth — a soft session-end (the /clear departing-session hook)\n 1246│    // must NOT delete a row whose owner is still alive (the surviving poll listener),\n 1247│    // or every post-clear send misses the lookup → NO_PERCH while ready reads healthy.\n     │...\n 1265│\n*1266│    // [unit->REQ-SOFT-END-PRESERVES-LIVE-LISTENER] the dead-owner control: today's\n 1267│    // cleanup is kept — a row whose registered pid is dead is removed at soft end.\n 1268│    #[test]\n 1269│    fn soft_end_still_cleans_a_dead_listeners_address() {\n     │...\n 1284│\n*1285│    // [unit->REQ-SOFT-END-PRESERVES-LIVE-LISTENER] erase stays unconditional: the\n 1286│    // hard wipe orphans any listener by intent, so its row dies with the endpoint.\n 1287│    #[test]\n 1288│    fn erase_unregisters_regardless_of_liveness() {\n## startup.rs\n 505│// [impl->REQ-HAZARD-LISTEN-ORPHAN]\n*506│fn listen_orphan_exit_line(id: &str, parent_pid: u32) -> String {\n 507│    format!(\n*508│        \"LISTEN_ORPHAN_EXIT:{id}: parent pid {parent_pid} is gone — exiting the orphaned \\\n 509│         listener so the perch flips OFFLINE (its recorded pid dies) and dead-owner rebind \\\n 510│         unblocks [REQ-HAZARD-LISTEN-ORPHAN]\"\n 511│    )\n    │...\n 527│        if parent_is_gone(parent_pid) {\n*528│            eprintln!(\"{}\", listen_orphan_exit_line(&id, parent_pid));\n 529│            std::process::exit(EXIT_PARENT_GONE);\n 530│        }\n 531│    });\n    │...\n 870│    #[test]\n*871│    fn listen_orphan_exit_line_names_self_and_parent() {\n*872│        let line = listen_orphan_exit_line(\"flynn\", 4242);\n*873│        assert!(line.contains(\"LISTEN_ORPHAN_EXIT\"), \"carries the tell\");\n 874│        assert!(line.contains(\"flynn\"), \"names the listener\");\n 875│        assert!(line.contains(\"4242\"), \"names the dead parent pid\");\n 876│        assert!(line.contains(\"REQ-HAZARD-LISTEN-ORPHAN\"));"},"isError":false,"timestamp":1784197566645}}
{"type":"message","id":"93992d21","parentId":"b3aac80e","timestamp":"2026-07-16T10:28:55.022Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"**Clarifying adapter listener status logic**\n\n**Identifying stale registry entry risks**\n\n**Verifying message delivery fallback behavior**"},{"type":"thinking","thinking":"**Confirming Windows loopback TCP unrestricted**\n\n**Clarifying registry attachment vs local address handling**\n\n**Considering api ready sharing listener adapter**"},{"type":"thinking","thinking":"**Analyzing TCP and WAN message flow**\n\n**Ranking hypotheses for TCP receive failures**\n\n**Designing elevation token test matrix**"},{"type":"thinking","thinking":"**Designing elevated-agent integration test**\n\n**Designing persistent listener subprocess**\n\n**Implementing deelevated sender fixture**"},{"type":"thinking","thinking":"**Planning event capture via file**\n\n**Designing detailed architecture summary**"},{"type":"toolCall","id":"call_kmrVqVWUmeW3HFpwd8zWLFiY|fc_017bff223290b7ba016a58b1f4e79c819ab90b8e81311010bd","name":"yield","arguments":{"type":null,"result":{"data":{"summary":"Finding: “elevated endpoint” is not an endpoint state in spt-core. There is no elevation bit in `InfoJson`, the local listener registry, the subnet `Instance`, the spool, WAN message records, or RC/session resolution. The only implemented elevated-agent topology is an externally/harness-hosted elevated process. Its `api listen` child inherits that process context, binds a loopback TCP relay, stamps `controllable = Some(false)`, and can receive messages through the same TCP→relay/stdout or spool→relay/hook paths as any ordinary harness-hosted endpoint. `docs/DEFERRED.md:27` states this explicitly: harness-hosted-elevated “works: TCP listeners + talk-down pipes.” By contrast, `spt rc` attaches only to broker-owned PTY sessions. A harness-hosted endpoint—elevated or not—has no broker session and is intentionally advertised as `harness_only`; RC therefore fails with “no live session” even while messaging remains valid. Spt-hosted endpoints have broker sessions and support RC, but are unelevated by construction because the daemon/broker is forcibly de-elevated (KNOWN-HAZARDS 5.7). Therefore the observed RC failure is explained by hosting topology, not elevated-message incapability. There is no code-grounded evidence that elevation alone blocks message receipt. For a message failure tied to a hyphenated/shortform invocation, the leading explanation is parser/target conflation before routing; the deterministic discriminator is to hold one hyphenated harness-hosted listener constant and compare a direct `spt_msg::deliver::send_windowed` call, canonical `spt send <target>`, and the shortform invocation. Direct+canonical success with shortform failure proves parser conflation; failure of direct delivery proves listener/state/transport instead. No files, endpoints, daemon state, or live `hertz` process were mutated, and no tests were run because this assignment was explicitly read-only.","files":[{"path":"CONTEXT.md","description":"Canonical topology and lifecycle model. Lines 29-39 define daemon/broker/brain and the harness-hosted in-session relay; lines 180-183 define `api bind`, long-running `api listen`, and short-lived `api poll`; lines 232-236 define ReadyAgent/LiveAgent; lines 663-665 define Unbound as broker-session-live but not message-addressable; line 767 mentions an elevated endpoint as a privileged agent process, not as registry state."},{"path":"docs/DEFERRED.md","description":"Decisive elevation statement at row 27: spt-hosted children are unelevated by construction; an admin-capable agent is harness-hosted-elevated today and “works: TCP listeners + talk-down pipes”; true elevated spt-hosting requires a future consented PTY-host satellite."},{"path":"docs/KNOWN-HAZARDS.md","description":"KH 2.1/2.5 separate harness-owned PID liveness from daemon-hosted status liveness; KH 4.3 requires stale listener cleanup before address lookup; KH 5.7 requires the daemon always run unelevated in the invoking user universe. This rules out treating an elevated external harness and an elevated daemon/broker as the same thing."},{"path":"docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md","description":"Exact harness-hosted attachment design: SessionStart writes an adapter-agnostic seed keyed by parent PID; `api listen` consumes it and resolves adapter/profile at bind time from the parent executable/registry."},{"path":"docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md","description":"Topology split for receive: harness-hosted uses relay-poll; spt-hosted uses a broker-owned translation binary/PTY channel. Also states a poll listener, if present, wins over PTY injection."},{"path":"docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md","description":"Load-bearing RC invariant: attach is gated on a live broker session, not on perch online/bind state. Unbound is attachable but not message-addressable. This does not make harness-hosted perches attachable because they never have broker sessions."},{"path":"docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md","description":"Defines message window/channel/persistence axes. Important alternate explanation: `active_only` deliberately avoids the listener wake path, while `force_native` deliberately rejects harness-hosted endpoints because they have no translation-binary PTY channel."},{"path":"crates/spt/src/api/startup.rs","description":"Exact bind/listen symbols. `bind_from_seed` calls private `establish_perch(..., \"live_agent\", Some(false), ...)` for harness-hosted endpoints. `establish_perch` writes `InfoJson`, `ready`, session ledger, adapter/cwd/read-env, token—but no elevation metadata. `cmd_listen` resolves the adapter, binds `Relay`, optionally stamps daemon status online, emits `BOUND`/`READY`, drains backlog, and streams events to stdout. `cmd_bind` is the spt-hosted counterpart and stamps `controllable = Some(true)` for live agents."},{"path":"crates/spt-store/src/info.rs","description":"`InfoJson` authoritative endpoint record. It contains PID/session/type/status/home/adapter/controller/rest/`controllable` and fault fields, but no privilege/elevation/integrity field. `controllable` explicitly means broker-PTY hosting topology: true=spt-hosted, false=harness-hosted."},{"path":"crates/spt-msg/src/listener.rs","description":"`PollListener`: binds `127.0.0.1:0`, registers `{id,address,pid}` in the local SQLite registry, reads framed `(from,body)`, and unregisters on close/drop. No elevation check, token check, adapter check, or broker dependency is present."},{"path":"crates/spt-store/src/registry.rs","description":"Local listener registry schema is exactly `agents(id,address,pid,started)`. `register_address`, `resolve_address`, `clean_stale_entries`, and `unregister_address` carry no elevation dimension. Resolution stale-cleans then returns the loopback address."},{"path":"crates/spt-daemon/src/relay.rs","description":"Harness-hosted receive owner. `Relay::bind` owns a `PollListener`; `drain_backlog` claims non-deferred spool rows; `pump_once` accepts one live TCP message then drains backlog; `run` repeats. It is stateless and respawn-safe."},{"path":"crates/spt-msg/src/deliver.rs","description":"Local delivery core. `deliver_tcp` resolves the local listener address and counts only a completed framed TCP write as live success. `send_windowed` performs TCP-first except for `active_only`, then durably spools. No elevation branch exists."},{"path":"crates/spt/src/cli.rs","description":"`Cmd::Send` has a positional `target`; `cmd_send` first tries only the spt-hosted inject leg, then falls back to `deliver::send_windowed`, and finally WAN only on `NoPerch`. A harness-hosted target returns `None` from the inject predicate and therefore proceeds normally to TCP. `--force-native` is the intentional exception: it rejects a harness-hosted/non-controllable endpoint rather than falling back."},{"path":"crates/spt-daemon/src/inject.rs","description":"`is_spt_hosted_no_relay` is the shared topology discriminator: online, no local relay address, and `InfoJson.controllable == Some(true)`. Unit coverage explicitly proves `Some(false)` harness-hosted endpoints are excluded from PTY injection—not excluded from TCP relay delivery."},{"path":"crates/spt-store/src/spool.rs","description":"Durable fallback and drain truth. Rows carry `(from,body,window,channel,ephemeral,delivered)` and WAN op dedup. Normal messages use `WINDOW_DEFAULT`; active-only mirrors to `deferred=1`; relay drains non-deferred; hook poll can include deferred; taken rows are marked delivered rather than deleted. No elevation metadata or privilege-conditioned query exists."},{"path":"crates/spt/src/api/delivery.rs","description":"Short-lived receive path. `cmd_poll` resolves Hook eligibility and emits whole `<EVENT>` envelopes; `poll_drain` claims the active-window rows. This is the fallback/active hook consumer independent of the long-running listener and independent of elevation."},{"path":"crates/spt-daemon/src/registryhost.rs","description":"`RegistryHost::advertise_local` converts perches to subnet `Instance` rows. It derives `bound` from liveness and `harness_only` from `state == live_agent && controllable != Some(true)`. It advertises status/type/adapter/project/controller facts, but not elevation."},{"path":"crates/spt-net/src/net/registry.rs","description":"Subnet `Instance` schema carries node/status/epoch/resource/recency/type/bound/controller/`harness_only`/adapter/projects. There is no elevation bit. `harness_only` is explicitly the remote display datum for an online LiveAgent without a broker PTY seat."},{"path":"crates/spt/src/wansend.rs","description":"Remote route: `wan_send_with` parses the canonical address, resolves a visible `Instance`, refuses own-node stale hits, dials the selected node, sends `WanMessage`, and waits for a receiver acknowledgement. RC uses sibling `resolve_and_dial_owner`, but routing to a node does not create a broker session there."},{"path":"crates/spt-daemon/src/wan.rs","description":"Remote receive: `receive_wan` applies access and op-id gates, then tries the same local `deliver_tcp` relay path, then spt-hosted translation injection, then an atomic spool+dedup transaction. No elevation condition occurs anywhere in the funnel."},{"path":"crates/spt/src/rc.rs","description":"Exact RC production path. `run_attach_inner` rejects only explicit offline status, then `establish_attach` connects to the existing broker, calls `resolve_session` against the broker session table, and only on local miss resolves/dials a remote owner. A local registry row owned by this node but absent from the broker session table degrades to `OwnerDial::NotFound`; result is “No live session.” This is expected for every harness-hosted endpoint."},{"path":"crates/spt-daemon/src/harnesshost.rs","description":"Spt-hosted ownership path: daemon prepares and asks the broker to spawn the adapter’s session command into a broker-held PTY. This is what creates the session RC can attach to. Since the daemon/broker identity is unelevated, these children are unelevated."},{"path":"crates/spt-daemon/src/deelevate.rs","description":"Daemon privilege invariant and pure seam. `plan_spawn` chooses de-elevation when possible; module docs explicitly say elevated one-shot clients talking downward to an unelevated daemon are valid, while the daemon itself must not inherit elevation."},{"path":"crates/spt-store/src/liveness.rs","description":"Single liveness resolver. `STATUS_UNBOUND` is broker-attachable but not message-addressable; online daemon-hosted liveness is status-authoritative; external/status-less liveness is PID-based. No elevation dimension."},{"path":"crates/spt/tests/contract_e2e.rs","description":"Best existing adapter/listener seam. `seed_then_listen_binds_and_relays` already drives real binary `api seed → api listen`, verifies a spooled message reaches stdout, and verifies the bound session/type. It should be extended or mirrored for the isolated discriminator rather than inventing a new harness."},{"path":"crates/spt/tests/quickstart_e2e.rs","description":"Best existing live-routing seam. Starts a real `spt ready`, invokes canonical `spt send`, asserts `SENT`, and reads the exact `<EVENT>` from receiver stdout; then proves offline spool and restart drain. Missing dimensions are a hyphenated target and privilege/topology comparison."},{"path":"crates/spt-proto/src/id.rs","description":"Endpoint IDs explicitly permit internal ASCII `-`; tests accept `kebab-case`, `doyle-psyche`, and mixed IDs. Thus a correctly passed hyphenated ID is valid downstream. A failure limited to invocation parsing occurs before this lifecycle."},{"path":"crates/spt-proto/src/addr.rs","description":"Canonical `[subnet:]id[@node]` parser validates each component through `validate_endpoint_id`; internal hyphens remain legal and unambiguous."}],"architecture":"## 1. Production ownership and receive flow\n\n```mermaid\nflowchart LR\n  EH[Externally launched harness<br/>possibly elevated] -->|SessionStart hook| Seed[api seed<br/>daemon SeedRegistry keyed by parent_pid]\n  EH -->|starts child| Listen[api listen]\n  Seed --> Listen\n  Listen --> Establish[bind_from_seed → establish_perch]\n  Establish --> Info[info.json + ready + token<br/>controllable=false<br/>NO elevation field]\n  Listen --> Relay[Relay::bind]\n  Relay --> PL[PollListener<br/>127.0.0.1:ephemeral]\n  PL --> LR[(local .registry<br/>id,address,pid,started)]\n\n  Send[canonical send / WAN receive] --> Resolve[registry::resolve_address]\n  Resolve --> TCP[deliver_tcp framed write]\n  TCP --> Relay\n  Relay --> Emit[emit canonical EVENT to stdout]\n  Emit --> EH\n  TCP -->|miss/refusal| Spool[(perch spool.db)]\n  Spool -->|relay backlog| Relay\n  Spool -->|api poll hook| Poll[cmd_poll/poll_drain]\n  Poll --> EH\n\n  RC[spt rc] --> Sessions[broker sessions map]\n  Sessions -->|harness-hosted: no row| NoSession[No live session]\n```\n\n### Elevation passage\n\nThere is intentionally no “elevate endpoint” transition through these stores. Elevation remains an OS property of the externally launched harness/listener process. `establish_perch` serializes topology and identity, not OS privilege. `PollListener` uses loopback TCP and the shared local registry. The daemon is separately forced into the unelevated user universe. Therefore:\n\n- **Harness-hosted, ordinary:** external harness owns terminal; listener child; `controllable=false`; messages via TCP/relay or spool/hook; RC unavailable.\n- **Harness-hosted, elevated:** same persisted/runtime topology as above; the listener inherits the elevated harness context; messages use the identical TCP/relay or spool/hook path; RC unavailable.\n- **Spt-hosted, ordinary:** broker owns PTY/session; `controllable=true`; messages prefer relay if one exists, otherwise translation-binary injection/spool; RC available.\n- **Spt-hosted, elevated:** not implemented. The daemon/broker de-elevation invariant makes its children unelevated. A future consented elevation satellite is explicitly deferred.\n\nThis also explains an easy observational trap: comparing an ordinary **spt-hosted** endpoint with an elevated **harness-hosted** endpoint changes two variables at once. RC differs because hosting topology differs, not because privilege differs.\n\n## 2. Exact call chains\n\n### Harness attachment/listener ownership\n\n1. Adapter SessionStart invokes `api seed`; daemon holds a `Seed` keyed by stable `parent_pid`.\n2. Adapter launches `api listen <id>` under the harness.\n3. `startup::cmd_listen` resolves adapter/profile and calls `bind_from_seed` (or the explicit-session fallback).\n4. `bind_from_seed → bind_taken_seed → establish_perch(..., \"live_agent\", Some(false), ...)`.\n5. `establish_perch` writes `InfoJson { pid = listener process, parent_pid = harness, session_id, state, adapter, controllable=false, ... }`, `ready`, ledger, and capability token. No privilege field is read or written.\n6. `cmd_listen → Relay::bind → PollListener::bind_blocking` binds `127.0.0.1:0` and calls `registry::register_address(id, addr, current_pid, owlery)`.\n7. `Relay::run` owns the listener until killed/orphan-watch exit; startup and each cycle drain durable non-deferred backlog.\n8. `startup::emit` formats delivered `(from,body)` into canonical EVENT lines and flushes stdout into the owning harness session.\n\n### Local send\n\n1. Clap `Cmd::Send.target: String` is handed to `cli::cmd_send`.\n2. For non-force-native sends, `cmd_send` may attempt `try_spt_hosted_inject`; a harness-hosted record (`controllable=false`) returns `None`, which is a normal fallthrough, not failure.\n3. `spt_msg::deliver::send_windowed` checks existing/online perch.\n4. Unless `active_only`, it calls `deliver_tcp`.\n5. `deliver_tcp → registry::resolve_address` stale-cleans `.registry`, connects to the registered loopback port, and writes the structural frame. Completed write yields `SENT`.\n6. A TCP miss writes the message to the per-perch spool with its window/channel/persistence tags and yields `QUEUED`.\n7. Relay startup/cycles call `drain_non_deferred_audited_at`; hook consumers call `api::delivery::poll_drain`. Thus losing/restarting the listener changes immediate-vs-queued, not deliverability.\n\n### WAN send/receive\n\n1. Sender `wansend::wan_send_with` parses canonical `Address`, resolves a visible subnet `Instance`, dials its node, writes `WanMessage`, and waits for receiver acknowledgement.\n2. Receiver dispatcher calls `wan::receive_wan` with the QUIC-proven origin node.\n3. `receive_wan` applies access, perch-existence, op-id dedup, and user-message restamping.\n4. It calls the same `deliver_tcp` local relay leg first.\n5. Only if TCP misses does it try `inject::try_spt_hosted_inject`.\n6. Any remaining miss atomically commits WAN op-id + spool row. Elevation never participates.\n\n### RC\n\n1. `rc::run_attach → run_attach_inner → establish_attach` connects to an already-running broker.\n2. `resolve_session` queries the broker’s endpoint-labeled sessions.\n3. An spt-hosted endpoint has a broker session; local loopback attach proceeds.\n4. A harness-hosted endpoint has no broker session. If its subnet registry row resolves back to the same node, `wansend::resolve_and_dial_owner` intentionally returns `NotFound` because local session lookup already missed. Remote routing reaches the owner node but server-side endpoint→session resolution still misses and refuses.\n5. Result: no live session. This is correct for both ordinary and elevated harness-hosted endpoints and says nothing about TCP listener delivery.\n\n## 3. Invariants another agent can test directly\n\n1. **No privilege state:** `InfoJson`, local `.registry`, subnet `Instance`, `WanMessage`, and spool schemas have no elevation/integrity field.\n2. **Topology is explicit:** `controllable == Some(true)` means broker-owned PTY/spt-hosted; `Some(false)` means harness-hosted/no broker PTY. `RegistryHost` publishes the latter as `Instance.harness_only=true`.\n3. **RC source of truth:** attachability is broker-session existence. Neither `ready`, local listener registration, registry `Active`, nor privilege grants attachability.\n4. **Live-message success:** a completed framed TCP write is the only relay live-success criterion; privilege is not inspected.\n5. **Durability:** a TCP miss spools; relay backlog or hook poll claims it later. The listener holds no in-memory message state.\n6. **Window exception:** `active_only` intentionally does not touch TCP/relay and waits for hook poll. A test using it is not testing ordinary receive.\n7. **Channel exception:** `force_native` intentionally excludes harness-hosted endpoints; its `leg=cli-gate-not-hosted` is topology truth, not an elevation failure.\n8. **Spt-hosted elevation is impossible today:** daemon and broker run unelevated; a broker-spawned child inherits that identity.\n9. **Hyphens are valid downstream:** `validate_endpoint_id` permits `-`, and `Address::parse` preserves an internal hyphen. If the target string reaches these layers correctly, no lifecycle branch rejects it for containing a hyphen.\n\n## 4. Ranked falsifiable mechanisms\n\n### 1 — RC failure is expected harness-hosted topology, mislabelled as elevation (very high confidence)\n\n**Mechanism:** elevated agents are currently externally/harness-hosted. They have `controllable=false` and no broker session, so RC cannot attach.\n\n**Falsifier:** show the affected endpoint in `Brain::sessions()` / broker `SessionsReply` under its endpoint ID. If it has a real broker session yet RC says no session, investigate RC resolution/stream lifecycle. An `info.json` with `controllable=false` plus no broker session confirms this hypothesis.\n\n**Distinguishing evidence:** ordinary harness-hosted endpoint should fail RC identically; ordinary spt-hosted endpoint should attach. Message delivery may succeed in both.\n\n### 2 — Shortform/target parsing never passes the intended ID to canonical send (high for failures limited to a particular spelling; outside this lane’s grammar scope)\n\n**Mechanism:** parser conflation occurs before `cmd_send`, so registry/spool/routing never sees the intended target.\n\n**Falsifier:** invoke canonical `spt send <exact-id>` or call `send_windowed(<exact-id>,...)` against the same live listener. If both fail, grammar alone is false. If direct/canonical succeeds and shortform fails, grammar is proven.\n\n**Distinguishing evidence:** no new spool audit row for the intended perch and no listener frame after shortform, while direct/canonical produces one. Downstream ID validation already accepts internal hyphens.\n\n### 3 — Listener never attached or lost ownership (medium)\n\n**Mechanism:** `api listen` failed before/at bind (`ADAPTER_UNRESOLVED`, `NO_SEED`, `HOME_REFUSED`, conflict), or its parent-watch exited. Without a live local registry row, normal sends queue rather than emit immediately.\n\n**Falsifier:** observe `BOUND:<id>` and `READY:<id>`, `InfoJson.session_id` and `controllable=false`, a current `.registry` row whose PID is alive, plus a successful direct `TcpStream`/`deliver_tcp` write. These establish the complete receive attachment.\n\n**Distinguishing evidence:** listener failure affects ordinary and elevated harness-hosted endpoints under the same adapter/bind conditions; elevation is only correlated if it changes parent PID, adapter resolution, or state universe.\n\n### 4 — Elevated process and ordinary clients resolve different SPT homes/user universes (medium-low but important on sudo/alternate-admin shapes)\n\n**Mechanism:** the endpoint binds under one `$SPT_HOME`/profile while sender and daemon inspect another. On Unix, an independently sudo-launched harness can inherit root’s HOME unless explicitly anchored; on Windows, over-the-shoulder credentials can mean a different user profile. This creates two registries/perch trees, not an elevation-sensitive route.\n\n**Falsifier:** compare canonical resolved `spt_home`, owlery path, node identity, `.registry` path, and endpoint `info.json` path from listener and sender. Exact path equality falsifies this.\n\n**Distinguishing evidence:** ordinary/elevated records appear in different roots or node IDs. If roots are identical, this mechanism is gone.\n\n### 5 — Send modifiers intentionally choose a non-listener channel (medium if modifiers were present, otherwise low)\n\n**Mechanism:** `active_only` spools for hook poll and never wakes relay; `force_native` refuses harness-hosted because there is no translation-binary PTY channel.\n\n**Falsifier:** repeat with default canonical send. Default success and modifier failure is expected contract behavior.\n\n### 6 — Legacy/stale elevated daemon blocks ordinary IPC (low for current code, recognizable fleet-state issue)\n\n**Mechanism:** pre-KH-5.7 elevated daemon/named-pipe ownership can deny unelevated clients. This would impair seed/broker commands broadly, not one endpoint’s relay TCP route.\n\n**Falsifier:** ordinary endpoints and daemon status/seed/broker IPC all work from the same unelevated client; current daemon PID/token is unelevated.\n\n### 7 — OS integrity boundary blocks loopback TCP from ordinary sender to elevated listener (very low)\n\n**Mechanism:** hypothetical platform restriction on connecting upward.\n\n**Evidence against:** delivery is plain `127.0.0.1` TCP with no integrity/elevation gate, and the accepted deferral explicitly says elevated harness-hosted TCP listeners/talk-down pipes work.\n\n**Falsifier/proof:** an elevated listener plus de-elevated sender on the same `SPT_HOME` completes `deliver_tcp` and receives the frame. This is the only rung that directly tests OS token behavior rather than source equivalence.\n\n## 5. Minimal deterministic isolated discriminator\n\nUse the existing `contract_e2e::seed_then_listen_binds_and_relays` and `quickstart_e2e` patterns; do not touch a live endpoint.\n\n### Platform-independent core integration test\n\n1. Create a temporary `SPT_HOME`.\n2. Use a valid hyphenated ID such as `elevated-probe`.\n3. Start the existing in-process seed daemon fixture.\n4. Seed a live anchor PID and spawn real binary `spt api --adapter mock listen elevated-probe --parent-pid <anchor>` persistently (not `--once`), or use the explicit `--session-id` fallback.\n5. Wait for `BOUND:elevated-probe` and `READY:elevated-probe`.\n6. Assert the written record has the intended ID/session and `controllable == Some(false)`; assert local registry address and owning listener PID exist.\n7. **Routing baseline:** call `spt_msg::deliver::send_windowed(\"elevated-probe\", \"probe\", \"direct\", owlery, WINDOW_DEFAULT, false)` from the test. Assert `SendOutcome::Sent` and read the exact EVENT from listener stdout.\n8. **Canonical CLI:** pipe `canonical` to `spt send elevated-probe --from probe`; assert exit 0, `SENT:elevated-probe`, and the second exact EVENT.\n9. **Shortform candidate:** run the disputed shortform against the still-running same listener and same payload shape. Assert the third EVENT.\n10. Inspect `spool::audit_rows_at` only if a route queued: it identifies whether the intended perch was reached and which carrier claimed it.\n11. Separately assert the endpoint has no broker session / is `harness_only`. An RC no-session result alongside successful messages is the expected topology, not a contradiction.\n\n**Interpretation matrix:**\n\n| Direct core | Canonical CLI | Shortform | Conclusion |\n|---|---|---|---|\n| pass | pass | fail | Parser/shortform conflation proven; routing and receive lifecycle healthy |\n| pass | fail | fail | Canonical Clap/CLI target plumbing issue before delivery |\n| fail | fail | fail | Listener registry, state universe, liveness, permissions, or transport issue |\n| queued then backlog emits | queued then backlog emits | varies | Listener was absent/raced, but durable delivery lifecycle works |\n| messages pass; RC fails | messages pass | any | Harness-hosted topology exactly; elevation not a message blocker |\n\nThis is deterministic and hermetic because it never needs UAC/sudo: elevation is deliberately absent from core routing, so the test fixes topology and lexical target independently. It is the minimum test that separates parser from routing without conflating RC.\n\n### Optional real-token boundary rung (Windows/Linux acceptance, not ordinary CI)\n\nTo directly establish the OS claim, run only in an isolated temp home on a pre-elevated fixture runner:\n\n- listener child inherits the elevated test token and binds `elevated-probe`;\n- sender child is launched under the unelevated linked token / selected sudo invoker against the exact same explicit `SPT_HOME`;\n- sender uses canonical `spt send`, listener writes the received EVENT to a test-owned file/stdout;\n- repeat with both children at the same privilege as the control.\n\nFour factual observations must be recorded: actual listener and sender token/euid, identical resolved `SPT_HOME`, registered loopback address/PID, and received EVENT. This rung distinguishes a genuine OS integrity/ACL effect from source-level equivalence. It must not be simulated with an environment flag because production has no elevation flag to simulate.\n\n## 6. Bottom line\n\n- A blanket statement “elevated endpoints cannot receive messages” is unsupported by the implementation and contradicted by the accepted architecture note.\n- `spt rc` failure is expected for the only currently supported elevated-agent topology: harness-hosted.\n- The code intentionally models **hosting ownership**, not elevation. Compare like-for-like harness-hosted endpoints before attributing any difference to privilege.\n- A canonical/direct send to the same hyphenated listener is the decisive non-mutating design seam. If it succeeds while shortform fails, no spool/routing/adapter work is warranted; fix the parser path only.\n- Actual live elevated-token behavior was not exercised in this read-only investigation, so the runtime statement beyond the accepted doc/source equivalence remains `[INFERENCE]` until the optional isolated token rung is run."}}}}],"api":"openai-codex-responses","provider":"openai-codex","model":"gpt-5.6-sol","usage":{"input":5429,"output":8921,"cacheRead":223744,"cacheWrite":0,"totalTokens":238094,"reasoningTokens":2476,"cost":{"input":0.027145000000000002,"output":0.26763000000000003,"cacheRead":0.111872,"cacheWrite":0,"total":0.406647}},"stopReason":"toolUse","timestamp":1784197566664,"responseId":"resp_017bff223290b7ba016a58b1bf1564819aa23799f07f6c4e6c","providerPayload":{"type":"openaiResponsesHistory","provider":"openai-codex","dt":true,"items":[{"id":"rs_017bff223290b7ba016a58b1c08384819a9f1a5cace63f4b53","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLHKMoyTo42LDT6alj44WrLYcx7MlDbJmyNvpfmRSB1ZcGZcNdMYm0QmEF5woxkQP9qOfog56OMKO3Kf9kWZWic0NiwbqWZkvFknRbp7VXimZEY30ZUEIfavUhPTT0ymvrxKH20dKVja8zwqMkKSBIGwoCXUvRCCreMaWT7Wtlphpl8PkJEsrz2Kvv4ixcu-t0RPzo9Z4_gd3Q7kCbHebO5sdt0wTAUAt6qZt4PvtvL3pBXel0UdHys2rqcnhi4Tfmm-8YcMaFwSKZ3zs8rdbsYjkmlcXdNp_UiIDkv9k59aA5_v6CgP_qCvuWYbHSlvfEkWYras-a4bl3EBkkKorbB09koCL9UmdlUg-Q1DaCQETXpebcZqugIzbMKujN3rdZ8HD0nwPt1wwtp8-av_IKY2mjXVjuEbOH5JOAoruzu2DpSBn0eYWYPBTvz8OL1o4sr8NRM4MEdx4GyRPzT2KIUyL3CZNh5IJDBPfl9iilvDjDZNzLJ6rygivUKJJ5AQtshX3X1k0WA5rmiHAOx9ifoRs12DynzireoKcy2rqZftWaQcrTqTKK7BrcPj5EpBcOUhJMCvLUNeEoSN5SIzIau_iAe1G48Mn9B7HfwFSD_DjoD_erVqOIf1QLx3kaefQSV0-_oloGZmvih6evY_PjAxWNRvtOMMMjW5nKu4mKYew3FfG9ZVouUuqEAwyViQO78nPq6Na4MxWeEBShHPnT_PqBeFB3oxkO4pQ6S_Z1U4fIDJ0AwXBq2vOi4XylSrsTtSpdlZKdX6TAS75194-kT-6mpRBJtkelCe4LklkQ4LKsCk1WttIic4jE59yWFFiE7gTnBU92nsiW7H-luh8iNa5bwiAyHjvXCuVbU_PllCpOcf7CEA-8BdFNxjDg1agN218tiVda5oYqSjGrruGQyv0TWAQdxS7PKe-9Frbqv7Nx7Ug6ee4XEYZF4pnbTs-haRI1vIOqfPp5FApfzQXAkskx6Tm3N-b9xsqtJqVp4MxR1uSwxX0N9jFcTzpRZVGeAUs2XCeouRahxiqqjjtiGxXUAR-IrsOTLz47fwUxZB0GIok_-WFUpnzOmiMeILTVqXnr7KX4I79Ocpc5Gy08m8xP8gNLhVcvZxZ7UJaxjL5SA057RwR3jCyLb3vQWxSJCnW8to2f0xMfcAM9eQv4r5EWZ_db-Sfn3Uf40K-QMrsee_TvnzT_eKUrH4aUT8wx27MB_rwQ_-tSOAYFyCxREkD5-gquIQPvlN1PFfwN1j01B1KPmP_3M85ZucfhqtN4F8l6O_pb328BTyt_eyw2wC1vieVVxpcGWqBog2TKl1NPfITOGAn51nCx9fpkLUST_j2L3RwBUMqdwB2ub-389kiQ40fPeLFqANCX07-dS6byMGxp3rrhKRrZIWVUnaA5Cu0t51oFF6bcDUyI3WLkmRcmTi-nKb0LxsF55VNeS4hRziKHnJv9Oq17q1jD0593N9f_7cpAQKcjP9ap-3vWIUDL-ilm_Hl1_rDQmv3w9kvvDGYXr_qLZs3BPWLiXB2pTiPtxWILaIjGz0Nq106oM-Hf9RngRQkDd_rLthlrwzZQSnEqbw72lLEG_lHmAmJspSXcO3XP8rNN2GH125ReDvpy5oQH-aCFBdxEWHRm-5Bss0QMgG5WxOtdnJxlKvO8Zdm3NnONzMN2Wjs1S_-Cl1sFovWjIby10JKTg9giZV7YJPJbrqEl4ukkPQEPkDHXjpXFr_keNI53ZS9F-FyHq1so4XoShls2yglAMVancsWSQZCEcUYqM6rssfxOqmxxuQRI7eGCFYIlAOxXkekCbz4NJuoOWuBxsxS8IbFBTLfVUebJk-HklExLEqR4yh2W1K4ddQL7s-7FsvRxVJPLLbW3lCUq12dkK3SmSbb2OV6GnnFDKttqGNQD9M3UJ1nkLSYSIohc65W-vFaBHs5gJF8Ey3RexAT54Do_i7zf0JShw_YedCzM30d3a5f9XxMVSS9gnVyDNPIztWecpd6kgM-CiZ1fmVI4SwcbTbrqkqCRFriipWLAi7GMMeq8Ndz2rHtTmE6a_eodlvlIh21Pn_WASQeRg8OVXnDq2eU2UEG43gzVRZQkPzXyDKKrz6MpsSqR1LOaB3Z1EX1l17cjNA3smIzl9vxOO18XBJbOJkqNEDcd-sVNFqQG41S5zO1Cp-MtY0HjpNiTiPcEo6UeWi_M9YRH2dR7rPy_rKkJfn1eJ_2JNgE3abax1hIE4yzUcU30xIbUi9C6fSkA7Yqg6ic_nb_9_rzWRlS0ANUquB8i9zUWG2iP7YS9-G9waU-4p_-E_1N4B0e83qxgQIf3gJIEB8IFajaGWYTX1jKCOo4vNGIqWwAAGEcXiWTOKgOCmQeXOYzKr9IFVSkhAI-4oMEBzvG0nk9yPSXMxPh5jI7RLWWOmlvkHDuBpSKrIpL98KHafyhI6hp3AlmtWuHyykEMuTdSgWLX0flAy6hDJHsGHan9blnZjYFcYy5wapu3P8MS_G_D6bCi05SS68_x1U4dD8Dv5DPBWEauz00zIjnRoP8d6OUckSyhVsC9qFYcMqAspbAVdaViaZ38iA4o4iOzgxN3KCbQyqh89qjVbGMJXUezAbNZA5hnZypoZWgk41ioDzIGMQpCFn3EM4Ig4UxOPTlJiw86stBGjID0f9_HD6QTHvgoxZ3mIC0PuzWVGWF7TJBIFjPM7-hFKVmNWxilQkljowTaGeYMutzopwZxC242VSssCgs5lJFioqtaJl_2HP-Sj0sPCG-MzSUhPGKcRVEMECtlGWMHJhR3hgeiIXIhhYJ0u5ciLe0TqKlrVtZq9kuD3eaaeg6-oF6pMtBSxdjH9YcXKIirbSPhtcE30LV9WU4_ZTCZwOiTxxmpixS25OM4IkApqfDm15jwvuChqUL2pKcte3rWEE6Y9CGXswyL_e3Jw92xBIiwW9LUzwkeN5mqk0hP8l6GQmH7_FhTcQS6XNZbui5V-LaNHRxBTq0HvnYGl4L2RPe0FI_l75Qk0JVcd7krG07c-LucE2zj4JdmG1LqDqRRuxNmWg7tkj8FgtKBMEEaofTcEYlXlJnOqkbTJvT-8HtHeEsAxU_uFojJnuyKGiCl_SvyLZe3RQfiy0NLbniNURnvi_xqfuwTZpA4ir_FFJwOkDWbD6yr03pXITQ655um4t2l-6cp3stmYQdT2lycYk8R6embGOENzL_z5BDBdike9Dugzs2PCU6-vtVMC0LQ61R4jXU2CpRK5qNHV-DvSO7Oo3-Pp9j_mwzPivaq5RI6nPGNONFgIKBqHaH7a50STEBoHHA-9opybkB2clssAq5ufSKOQRANyVc5shqrMgSBAEUAZsSc4o62yg8nq81HDX9dJkshM8CQ0bVCHsFyUq9KWnisio3EP7mxpuf2geh3G6AMLdFs47LkiUHcMBuJWfFKCKdDmTFNMX40eGv3zx3oCquXSqWA24JQqfJjQeopOzBikAHnVuF7D4bMuTTsanhRqNQ1ySQK0F1T4dtVCt99D70oebmXZRv8h3ybbxUW6MVaIupXqChQPeXNk9lWyLNA054hOFd6AAMIcIkhrnmfwkOjdEyoN0FBVa8VsElhY0lHbx0F_ttMfaAT0694Ir4PVJqqMAnEGMFugZAhxONew2t-J9hW3byCH-sxnjeFw9KB_DQMx7ohBV4mkWJ4Ep3Mr8lo9ZTfYHtBi7QGWynpfHS_BR1Y6SeeYLrQ0Bi9JXJGqRp6K6Gi-lKjkIJ4Iq31QatT7Z3O0yPcelp5QgRTa4lt4bETKn4uy8gSUH-nRMOpiER_YlhE7mrJxs_bD106ItBVP2mEmv8kn18xjwstmcFfryJc1DvdJOyjy_mUwr2T8aCAkZ2BSh0nA5RT2QM1xqhVTnnbZBmLXON7qutXNPW203TUsXcaeYw8seHFU5YSeBX2KImVhBnL9g36HHLmvbji6-t-zLZDiPLLNh-qk6tBGXwb__0yxLILVrgsOAGgeuID4xsGjVxAie-KqibQ53-ACSDb2V54hSero0DZWQTXoIrh5-rzTdtjsy7Fa8inR3E2B5-8NbT5BKEK-3-mKqLiGqAnn2RrDywDm8aH2ZGfhOPNxSerZR-ylBW9Wn87E7xnEi3vedNqtiC-8Z6WuvSB7JfaeaCAkDQ8gC0e-yZiV2-foroZ5QGMSA1ejBXBKpsfuqSu8sZ20qRi0E_O7HoaGFKghAV6iYzhhhFEAG_CGMFVReXeMkeewqSIZ80lprWt18_gPBAZ6X6T0eHwoQjMpVnq0dx7c4r1G5_-3sVEnl","summary":[{"type":"summary_text","text":"**Clarifying adapter listener status logic**"},{"type":"summary_text","text":"**Identifying stale registry entry risks**"},{"type":"summary_text","text":"**Verifying message delivery fallback behavior**"}]},{"id":"rs_017bff223290b7ba016a58b1ca8b7c819a8fc7adf85692e4b8","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLHUj6JRjwxFot84Fd1eMtKItajO1R0VCRVOb9558ZPsJSeLOEvuCU2lwWCmM4TnzX6D3S-LCZfnHTABON91Ao9UxD_0RmzCX-7YmgVzqeJwV53HMjNta5IcQIWzf1HSGD8SksaBIg6kBadX6ZqkCpWuT5cPzWTzpSqb-HXpBwOq9NkUH5kD87lw-CcNb0mduiRWkIYQPRyCd4lbnkKW7eixCWwQfskLU5d5zNiZYIzJdsvBSF3z4e0lQ7VrPiFEjlu0DJgF3zQ2KZRQWRa-UpVLKaWsGGdzce76oGp96bJVf43Q9kWkX2ubES8YJ7n2rfigd_oDd2gXCzNG_X2RCHDoh3gblVUYSvaEHRKgQoxl5JSTZ08NTMRHTynNLOUBDE_rPvGvrH1hYoHUGh8WefKcdXaMI0DsA4f2qMiQX2Vyni7MrNT7Gl4RUVCejElmcsm9kUYgEOwvVLSSA3oQu-HPc5RMiQv49qPZo8p4enNSbfEppp0B8BnCAccXsTpPsJimZLVUamzA3PvoczMtW_xa_6O-CRTl41cwEHOvMK8LoYXq6X2dQNBrXscl9L3F1YMV8RP4ck8CsAfN4hshF2SIlAR8BvKzaQ_rcvzGSWcctZQSJorU3Te1kR4bdurpAmX-4c9PLHlemh90PUd2PxeGIcbSxcTP4lW5kxvQKtXPLIJwZjh2RJ2wrBurFy6vm0FZjttK6cDXSG1PEBwumuJudB3Gy7guaHtr8pyks2bvQ_2L3-p9zr7_USNbeaRxsHWPNKM6AFppmIsAH534NURWpVIdQ-YYhss2iBbnbB5j7DDKHe23GcNMldvj0pQJ3PYqrUjuQV98IwTRsaqeHHCDfsJZTIDwOqyW-TIeZBLDe9PA3-F4uKX5MxEZAEv6PYUybe37wH9LN6VSYfsfnJiLXdX1fCBfoD4PczzfeTkOjnIRs0nY8RKUTKKPoH1nIlcrkz4jfaOm6yc36TVvfRklCrizXOoz7_ZNgDozzzjb0AGB0kotWTVgmj84sy0a66aQfY1ZfLX3Vb3s-qapszRjFdtVMN0H91OGju2YS3VnE0P8jgkghgwWnPEXFQKDVecwoWiBHN-uWA0SUSS85WXPL_-ePOWljU-kUQ34t4Y7XLItG-3fl7U0yDa8gQWeM-7Qis0MsvaV9-XcoO2xsQVK5xmIlnC4BIAkm59bBuXnrRY0W9nmfL1U_eWPKtq6qpruB-ryZUx7aqAAYhlZ9IdV59uY8EPt4BDhzUS_JZuERr7mPp_M5dYf3v_xzvlG-GNYl_LMOx-yBj-WPjX10zUgIuKoprWeHYxgUVod6vAga3-MTYoEDRsLz6OqBuFgdrhFip6L8qEFw0_wx2hXa0MI914m4EqV0NNG1uOJrRjRszqfdlVIV8PQ0Wdp6N9eT7_o_lnp95NZkBjEbyBa-8iVKOBTSr1O_ewSCIMMAgTnx-vTua2os5OYVHu1gQnKeEesQkIwKZe4rhNErd8ka0nJmuwm3uotToFp-f-eCwa7c917-_FFsLnji60P06wRsZLEfN1ehXoARVuPasmcJms-TxXch0MwVUvMn-nGzgMadKQVehDE97TWUBBkPpLzNdTCr7YjRpdfdwEUJkEw7TLOWmSJWYmpHQTk3a1vJyVgU6SFURAAiik9vmGYfJ3pPV0g-VJxWjEjiCvpiHEWPfXVUSyWyoRissGm-BWnQlFSsTSUTOC5wDqWzm0AaHaCa345_t5JTtq8i_ku_IF1x2yLB9SNTjk6Rpotmr3PI-_NCWqS5v2iPBJQfLpR_u0drXfSB4f_GClFyYhp6qdk8trF0v_FNlWjAbGdXyCG8grh6QadYqOYaaq0tbRrkyOYxiTw8Gf3PD3gaOPCHqDjcoRt8yEHrSKGbTZJevqIIIttm5ngq851zAToLCMn1qCsMRdqWXu40h7GM-UkPMbM3UkSPO00LnPSfYAJ5O9DQumuTTOo-YFjEW6b60E5DMz8hDIHxSwoT5NBy5LG-pBv9EX7tUN-3F1wZRj8MPSZJu0Gyb-EWeG97GPfh9IGBuXkhyUyuVWrDjbVJBh5YA1-d6EZBVBjC0Bu8lZGXccualaJ0yMl_iKIEEjLefcyaHJisLGsUB_04sBHCZSX1m_BBHvzlYyt8BaOu_ns_FRq7QJWxF-9gIfzomQKYPPFNuFaBYFQsBzuGo3YKKuiT-sfaWLFRALf3PoYs1paga7J9e_h0z6g2MSLJJfBhIJ4yYVQuS9xO38gm0SI0syHfUTYQstCcd3bfm1kQoUQcdg1X4WC8OCVckjiz9yHRKZFwsmphJohGzSv9w1MUkBn92aeFymEuQNxc8EDFCW6IGnin0--erCPDSv7tNXpud9SpGixssO9q0FiMaRbtXkpprg4ZyJ8-4otQJ0VRMChio11C-RbKchvBH3nCd8qBAuIyZuVB6Mn8Cl_GAkn80Dm-nD3cx6JRoU7_PG1oCatq7V_e9BDCkmBfgnwH0zple9MLAdB70Bpg6hezCfC7Rj_-CemkD7lNAPCiJU5ibTz1fyQ2UpOkbZtRIqiHp20viIJKx9r4DvHNo46MpBjGP4ksKnvud7WgTHlphe1Y4x0Gu8qFEfiomz9D-HKTxO9ekwuWfAn6HReImlwX8tFkT3zLKUx1wTgmwrLiW-Sif0BbEzzAQOe818ngLkbkYw5jufRz9Fj7XNOEXjmRLFUJag-ZnYrln99UuzwjfL1RplQNAgTiPdqEpETQCP41BKg21g3-I8Ng9Jwg1MR3eaC1Y2je8ScrvLYXy63_unJKMJsx9Y2Q6vzchZdT3cj0Hf3dEKhTn9Dik7LVvJj9WA3oOXzW1FXGGHlMAOXIFrn5P5LAJDZiRuTlmGppvAKV3Z91tsmY4KA29vaADYsjt7T4COlbg5yRkt6TF-k7JcO6Iz_bAnvMAq0JY4Fq7DL9LLOhjHMhFVl189pNXtCGx5ymo-UdLJ6a0TDL8mZFnFyrHjhyHh3xp2KWUIaN2B-xNQSucuVDTfplqJXNYwLc5Q8Vk10wPpMirbbslej39-pYAZVmkJTeSppv4S-kuG-Mc5-233dVk9u4WGpPlsjPFN_RzliO3sakTK1Vb86JrM9VaI0bPJnU6nD_Ho7MtGXu97sdHOfZ9CkwokFPd6H0E05wjNfDM38lOxJ6IVpKTvCdQ222S-PluTni-ddOyQsYPU9omqAg_zoTaP5okVf79C7wBvIgm7Tk1S0GFSBufUa57Gkx5-NKc0P_EyIs00rN_wYmNtb9DvfDVV_9XsAJT6wpT4vHj94vGZRNaHPNG8r7CJKLFrmx2y19lve8voMl_yztm1Rzy2x__BE_Z-3YSf9uQnxI1IwvavzSBGQhgnpwO0yA8cFwzLdVUbDpZzWp3rHpxk05mHV36eVNa1J7H9ATVwYxVakoDfovEvKCdM8NQ_6GtJvO7LxHQds9nWdS3xGEBZHV1FneZ9-Ys8qKn7wq7ARtyrujDViLfc92rBdyn0KLFTQfikqRQqHKTyWlovg1hFfmc5czV4Qh-7Jx1IgYFvlQN68SNFs1HPHihXpZE3HCG_TJRnbQPY4hmVM1VvyR-cPSDbYQzLQZFojrDeXpGwhWjHgnKsgrBekbPCweJsb_es7CIWd0LZ6ofyr6mHrY5IZbQKOlDz92vjIdky-9H-19QDeS7xOsp_5QBHee51XribHq3ne9Ou6esrUL99ISx-3w7Ob2UvTRA-gO03s8VedoGUc4q-pBQ9qzfWsCBHPmkRX4yfYRXnXc_jFC9agsYYEL6JCl-xnKKG9RYm64Au-fa9MCmOevDmFJmpPZipb2rPclv8TU97F_x07-6wrwe1iE8DSTcM33BTSimxNowrDdeCb_3F7D84OMhSMmnKi4hhW0TnK0wLV08Z--bfMxcTEJuTz66eHsEdjp1UHX05KH_dA_Nyzru-dHKo11YR2qWhRtC58rrK3YpWF-8VXv9MsWv2L6WrYnkullOim65m5BrKALdPkFgWMymRv9L0Ws-i9m0f6t_CE-0YwU_h7gmU6TrjHXlS69gtOLYKFWQrl24YbC8VultVRQ3YG-cZb7RD_u1TmEWYRBzVz8RFNgvm0qAYhk2b78b_IQll8mmgkoWFtSyCF6mgeLxswqO5U9btWnyUB59Ox7XWEIYiP5P3Bn4HepfrL","summary":[{"type":"summary_text","text":"**Confirming Windows loopback TCP unrestricted**"},{"type":"summary_text","text":"**Clarifying registry attachment vs local address handling**"},{"type":"summary_text","text":"**Considering api ready sharing listener adapter**"}]},{"id":"rs_017bff223290b7ba016a58b1d44aac819aad02433c5c9ef558","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLHfoaIZfTXe59sKZ2HVdtjWHcg-8v1NWa2a4-Y4B9_bF44anGwsKA6hyVAzJ2RWn1sXlz8pOum1EAV6xZ50WIfB58ZZVfzTGjbxvT_9iJyR_DUbeiGWt79xqzNlcBVRwaHFGgR5oa9O-e3fLha2b8kmiSPKvJu__6-VW9hRLNq4ktqqpok2E_ph9oPAQ6-xxr_I-PPMnE6Xryv4EHdQY_EN10Ll75xMe6EUSClLKbljNTRfkGEIYoq1XhNCqaMQEo8Af8WWJ0yQblXcGYIUuxEpn3k-WikW8xFO-72hUoyuLpWuKq-BF8JNhdZSfUYZFsQcgWVqSJ86i7OCVtqQ1eitVF2P8-QLPftKyK1cbjVUMpDku_Wu_4fxQvqevioIIIl74yq5esoRViuDqUTSn3IExOcYYJNsM3T1o4veyaNsI6QMkPszc-6nYWL5u1UrOCGU1afiCcPRVaCF-stTeDm4P8h3ohOZ6X6jkBVk202Q6xM7WJI8p_67sCZf14vWt5kssU4kHYaaSLNvym2GN1FjPmhxp460rFft1UFp0QYTxVQOk0xO4BZmqhxa2pWcEUW7JQN63YkMQGknbKaC0dcJC6Q91hpcZQDbKrfBuDGvrmFhEi5sJTdk7Qse09JvAuHEGaJyB3XQaMdtbt92TnsoTi5QPZZHNAOktKRwcchmDmXa4V3k8WHmhPv9q2QejYcvxKOlC_YISTQAnXsqk4ApZYZn-n1yRSkbWHDvbt50oGxvfsjYWIUhLLrn0HfaC14UVOX0JjkwS-whhIxYj0ESrlrBrIKAnLyt7q6aG-OsiSCrCxf6GF-kqKqEhdBJn6fKh8XnnBNcA7Nh0PSgZ0F7HOS2XrhqTIdUyxUTqlizdEa6kE_arRrPzSf3wWmzYSjMMSjYujU1ZzpuEirrAYy3RD58vFsjVTOjlUnea7X3wyWRnKf8zZLmZz-d0zIMka71-6xaTPTHx4qmNz7JNi2pRB4dX8Hbc94qheyXQFLgnxEp1yKRxWfhokO1QhUYyNfgHrzFE18vtTVgho7sgXb8ryvuMMOqx7JeP2aeONDODAab9hPerOvwum47cjzeKSq6nQzXsjRDkmxLvRhp1SI98kw2keNH51a6Ii7Oiuyg4NFuB2gCeIHvl5cXLt-Tqz9lzadaP8i-E-kjdUj3EXFweH1ldgauAmm1T57PA6C56S2GdNkeHT4HJ6CxLpVL9B9qFGtXyRjmqxeaVpkMLp3rDd07h6ks72BU-NeGbnZd0AZpXpQpdHyByE1tMM9YqUtAfzGUBkH3CSetV8SJhnxfMC5r5Nf5A7WDWQBtaZoGt7i03Gl_cwsuoGA42JIdbErf6nvHBF99BXSUHWNIaa0ryjc6a90lw20f0RHu8S4dADUtQsiUZAc2WNVeWmYCP6B9IUC0ZGxDMksdPH7pV93Lo17rekHbh3t3HpVT5SphckOXT27p25nvzaBk-UuM3VH3C2deI3Hy553eON1UyUXddvlMCN-E0GMNo2u7hyoDeKJJiResaNNLjRoYZd78Rr0WhpLMh9UEfXDkEibonTf6Iu1HgEnFhsLXFn5hYPJIYyhf-bop63NUdTGsHO3vQow6gOIcgD2IBgZ5C6rHiSnVLSV-0cslCB-65xSKfnCNyW6oAM_YZ2X7w4u4yJvzygxCLge8GtFJjBh6oaFfgE98x9F9EXpW6en-_1RVQ7exIG7geuMGzH9bAXCkEZzDp43RRbiHWMzEB01-F79msv9_G46PKAfdMyWs8-vSbdMzfK_ezlJGDp4Y99zEDJ8irniT0Kq05v6hzvR_OpqkcwfbcaZiuQMjS2RhxwfOnpN4xPi2Fr90CVUVlqimHZpi497NdjrmeTiLFUc8IHuCFXMjMuNDlo-E_yCbuKLW5w_Ysav2VydOkSZclztKYDAPMaGFtLUIXNnNhFzKXvxVgEv_a7Aefl3uCEQw4mOhlsS5n8UqqHRcJesDlRb6uYzdNBdLgus3ruNkzmG8ccXp2MMtdxzx6JpMuM6qyno1JYxpqMRfahcozEvyjD3npqHWvyKXgVKgIcheZuc8aFxrPxZOaa1hvzTRb0LAF5pu3AQ-T9WYQDGvdVRfKAMnOnQccJCmhV3bC3Vb6XgYLG0I6YMTj6X0Wnn8l35V8rlKcBtgGGcWYlsJZQGzRtfjRSEGsbMw4ao0GYVM6Of7U7Ffqt1qF9zi853c4Eg0VhW2LMJ576ZC37LmKoUo52_WnHtPxCInwySGoa57sNoI-5cLtmIDZzkhknL4Hk-rF_YYJua6KSCO26GGBqNHlfr2QXa3158JEzpGexRK19-IYFTQGW_hU9PFiNHfQ18pAcj5WhExrTKkUfnuZVRw0pBdeVUFGdwYQu8MwQ55REV6fNkdlnKvtWKSByAtcefrTmsRFXTrFS-3LTv2V1UuDX5Jyr3zekx1qiJVPdA9ZIAZWTTTX1SN319qFINy0x663H_o7Wz3H4xRZ4NjQp6OfA0fwuCeiK2GVTz_cPicPFnpz2SREjsbVMwMDknjWQm2tuH5fDGQMWi_n4ZR_6L-apDh8RssM36Rnt11vw4N99WGvmEjmm9fopD11ZCe_WyLmUMQS5rlHRJpa0teT39n3LIVX7hjnkvHWmOGcRWEeEjbXS91FPwiSJSE-MB77t7YRojH-aAfe6hJHp-B95S59fv8TiPsot0oU_-SsXEFTpypAtWT_jgVknvCT5GinBwJhATODd6hpYHL5k1HPQOxX_PQqTMQgVS68PSTZUNFVEt675Tkinq0-incJ67e7hPawam-oqEOtxWMMBZ7PfHzKvhDwFhqRxVkbTnIubB0vT71oSdEq5hZ8bSQf_I8e44hIFpG8Qg-dTxqPX-2tFMV3X9jymPdsoqSElAakxZ84H2LXuXRGFcDlGmDxfV3XwdpUsF74CB_GSgx6j6zh-c78EQsJN6FuwAdGKolM5UPzbNOt05Kaps0oYxUdTWCOOV2rEKxAZp29tcAMIOnhmAJTG5Tx_nwvU_H-52J_0lDCzMrNwN1mdzuC2y_zkXtYDaFOICjCsdz0X4NBxvWqn8kNOskxEsjouYUlQNLOuMImZXl3Ytenwt-zAQ45xgV2eJNbBTNTyKztM8wSPqRJ1QL93Lcl4SzZEcTKdO2qlVDg9wi2esXXkBu3DjHaouB_MGvLygBrz3dT4HcbNdJp4bYXJaHVWEs18LLEBf6nAONUWx1s06TLEbdlevhDv-gJ74iISjxSqWdkAslTzsMux1hu9bxsqqlWSIIalF2MkAxgf9GKTBZ55JTOVME1xkaadB6NU28phcN-wM3xLQ117ZlM6aJ1wclRq-2R1IxLIdVESZyR9OXVlYvhS85BHpu28xRZNiF9RbKw7SI_Bxt9FG71OhFyp7uUtRnqxx-Ftu3j1PzNGsikzQd-aSXsE8xw7q49xHKQUvEugevEcQx4-2BWgUNs1EPY6ob46bzaNJ1pXvBVMM2CijkCvmkxt_2YAj3fYO7uSZgcMlAcsQvziie6OV722zLjmMc5gbIah7BLOcEpbIMWgy5WPyVlyLDq0II6D6EkP4CmsAEqRLdIBxwRkg_4HpHVaB5Fyjsx7DLg5kithEjRZ76XM9VKEgLZQ4f924aPyf0qMGMgb3llWuXXDEznijrgnYO6IVcI9fjDn0uwZ3nuzjtskL3Hj2k5bHGyBxHOquxxxmliz7qVoqQDOCw98z9UIJM5ZSvUZrsMwwPQdu8gF_5f7UX-Kv--TzpaTvC9vtonEBRS0VzaNchXQC_6kXTx7rMduQ5h2GFILINhOGxFZ5I8NIMjYHgWp-1EzYU0qhnXWw-zrZ51cPQxrQwGG1n9P-5T_R1Mw52Kabnz0MtJ3DDZg04qKCLCd2YPcjgJBfKrhKnalQ4VW_A6zz1_Zv8v0D1rMkSntkJQxHtT6vQpjaZuaTlserMBQA4vvMYVXFmH0RF4RM8Kyh1oRVGakU-QlZj6C8HjQqAsECn8gI9tFYejTb0j4_5U1WRVcaDoQKUPVWkaXRUWFWVaKcEyhMHChw23k8Y8jeGAyZvgZt-itRIBtrX1ZNFp_z0GbbQ42-sJgkvyw3PiBFQUOP67f95BOsCjjWTh65Mnc4Pquf1vkngVxn1gSUijrYM11W_7qZH5S30RvJY895zhkTceoskuMvMZS5G1CGkSCLeXE1s0IsyOYKOqk0znlvtHVAxP_6ATjdNAvbOzq9ge3zPcs9TbrxoV-SvKSukISz0dYjDQawkNxcNm0r4Q05kALoIcqFI0vY-BbUsG_kgMiORuj02k6vLGgm0PgVTPvJ51Vo0dOA2bg==","summary":[{"type":"summary_text","text":"**Analyzing TCP and WAN message flow**"},{"type":"summary_text","text":"**Ranking hypotheses for TCP receive failures**"},{"type":"summary_text","text":"**Designing elevation token test matrix**"}]},{"id":"rs_017bff223290b7ba016a58b1df9d9c819a92fa422ef17b962d","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLHtx635pJ9xpMF2Q8xITrVTghZu0xU1xgmOOdRgqH5FIGOxA9MUnoAIvVj3PVsPUNaTjGXa3xzrdwhVmf1aVIUxAvObHKy91zgw6XIRJYzYcaYPmwgP7Jy35XrCZ2hwRbLFB-6wgiKDe6QAoN5LI4g9wh8pq9oBRy4ZZf5uwsg0mgvzpQRmiiI_fZ9r8QyzlixFPk5mWOyzp79MFvLGWkTla6JgQnTmayUg4rN2U-fUieVTJTUXgJOBfqI9ZSd1ujBXQfeeBFPjoxxUTRwGboJWvMB7J3xSTlJmJmLU3e7RIEEiijFKKOGFE1zQrky2SOQLrT9HnhsiFEBGRBsF4aTuIqP9uSwreWgBncUTBWTd1yM6vK1el_F7LynRIGLtBIvVlE33ftmVYk2zTrU_-dTCyoA0IGRaIkAXms5CSdPp6FzVDHAJTenJfRXQJGyjR0U0dzhkJH0PVFs4yAYE1jt4340IJk47i6GXoi8fCYJDnJ4VQZgDGpa4DTjrj4KF9sWat5awHMddxJaTZhXeujST3UVS6xaKFhPX0zRGcLrQjv16W7QhE0FS1RKOMQB6jmFOQ2F9AXlvWUVzNqCHne-K04eHAs7Hpe-7uOZYIBVN_I7GEv9kLtWBnxZOv22XOHFQ18_pog99Itrj3EqHKkXbF3uRoS486asqf1jkFrckGiDoY6evS2WIftKuYWKgnqUsii-7cjd0yqNOD6ZsZJAJwKymR2jLx6-YlCxMpGvBd0eVMspOWN0HRvKt6UkgS-uJiL6B34uBrJDYVaAm3NJPhoDg3fe524IP8gFMGy1in_Mv-9c6TDShZm0xqaKrDkFoAMfK-VY2DboNODbn8IiLiS6ZMNUg_yJZ_ZrgMGxozR61LseXPVI206jczNn7RfoUMEmrR5DITBe_s688Mz2OFMziV4JRbMgcYrk03TF7htaGdzzk4dXSEziNRCUA4vohPDDw-xBnWsYJiFp8DGC-QUTbfDjsmDW0fSCA2-PNWvJy1KOrc8UYiCCKzeXZQPo3KCre_IpSeZSlMtaVrJePvgGUMkgOcJtJRYL_ASKRZ9c-qW8kbSR9Vlbv1Q5xhqoaZEvROHgZn6JGuo8mIYNvbEXXFFwXJPYdKzxaaWo-zxTwk1slkO2sEVJUUBYz_Z1RX3F_TMP4df1zZjpovouL1yBybMjKgD2IqC7gHu5cZzI7EhdrO8OvNXRO3g9yrwYNlJ0LRp_YvTmLEg6ytOKaVtpvqQ0ot0uY21Jj0_J-pp5DuqlsqIuzYyxnNGV4YZzA8sUNZjOfBN9SHnJcNAJYPdkhPGTjg2ruVR8W9X6CeOcR45cxQJQYPpveTRuBkIp59Md4l_ZXzPJmQcMt9eI--dcFsr8ZCDaEpRaCsEEkBEqO6MI51LS0Sn9n7CDRJFqgQWs3xb1TeK9u8UrXhaZJj72TFLASzjaiDam97B_CCz5S57M68fUCgAq9XraqlTDif3zZ7LgEuMSjTdFxndqgZwQ3V5N2CsVRTPqcqxW2ovxhhrtrc5qjsfjZSNAwPtYNFfO1Gn1hsJ5QYGDrB-hcsYvL5UQAMT2ScVlsnSCnWrC_HAeHRGABskjxGf4_fwhS3S52lg65Dfz4EoOkVzeyqRFryd3dSqHVAARmMWdX1huKlZiVgLxLDoLhad-tTPhG4BW2LQCWvfDO8v-LYPuewuKnMVTl4HNBfBXe933f-iS4U7BDfoCFCnoAQJOK4hDHSZJgqZoxV8slr5AIO7BbtAtZHvWpry22W0yfqjNYyLJMnl2Jz61INUrha6KSmF6TxwfL43lRNFat-toAsypqsym2e4ajhNtT3lLA8HKeAPNB_PVaxDQkuyAlSDSxM-4tfmi1XViPfkPleQQ-c_m2zjgUzAaVdYZPX5ShZqklIxEsoTJOoBebViefIlKwOfopLuY-AhhanTIpzilh3Jlsb5-0Yifz1fpdwe8w-vXhXUxYkFJSyUw4z65U5KSK-5RGlZEEP4oLeF4IpkkzuR4lrNYcc4DFfQdc4IKKIBvhW4cYTn6aGoDiOliwX4w8iQb08ALYKdQ4sFGWdi8AbMtQmrD13-frbOo0QqqQILNmbbvwFhfppi7LbHbgPQ7fc2RYmlFKUqTX5FI7hgUTtMKji2iBAYbraVFGq9Qx3NDuCgoSTfh4V8XwAN2Wfe24kLiOqziZiVqyDbMdddY7m0j1p4EC2tEEimhOr5Ad3sI9vPJhsYOfg8vYK77xgWnUCWbDjYHqsRh0jLtI-jXNAS77CjEJwbV6fbTsdkqoQNoFkRtGRnpGVS4oxVUX1aDfQ4AvKrR0f75J1Lug3Pyl84qSUVQmPKpo0c-KSRV1iV0h2fuZMK5TMff3p_hkrYJXd8xvW2Nr-oo-UmAg4gtiY_AILCMhkxsZk3GLA7l7UzPy4MOMioBzPZx7gUhtfaKv8eKdR4f2NXsTtSMsTIfBuEOxMx6rAPqXjrMlRMUZdIWuy8CPwGlm7P_Ub2_FxFgZi3qTDGjoAXMeb6VW2xrDbY0738Mb0KDs3_T4bGvxYMLVhQVzNSIqbF3WiT0Yq6KZep12bdt2KIJcRoGsyAQxYv1NBsl8nVm25Ye7b7OorILznfvkBkPMnmsX3VggqM2L2JYMxnSZaudzYB9R6UPocSVd-UTa84RFSPyE9PAUTSKjk0P6yEd4tcfwvJp-NmEcQFXQ184K8f6DnVMQpYwPrsekJoE0BljPRD7RNLPbw6g8HeGISqLVEXc8z-WuyRyL6ymxdsgSZMLgmfjRypypi4pCxXI7-iknqDP4jws28v4WFpj1-BPw97HWj19z_swWdrInv8CUET0lo0rwJF5c6_qrWYIRKzk8FgQCiRjtCG01oq5RksK7k2Iiy5ex9zK79y6oPdZdOQN2hQ3S-pHbddgC-gjliZZOiPz7Tx3qxho6QvGbMckvF0pRP0KCPSSkZb2qg9y55ZAbkzXvb9UMGWR5X8tkBk6LoGnVVxGvu6DAfwtSN_5ht7LoSA9GzRIRcFLMgtvyuIJ8gKKZLSIh527tS7yBOzBtlW3tzttnthpyt1LBikuRjEXBcA43g5mx1f5npxPqqg-crgUMlsvWW3bL2T7Wwn_ARVe0qwPQVobyOngi1417Gq__UPBUj8ipWOa2cN72oMs_iFLfjn7tD79UzimgIuU7wZI9ktsPUchogWJC40F8H4MDpFXH6AUImB3h6kHunE7PA6GZq4lXEeU0146Io2h2fWFLLgm-abERIAzYBj7X9q4-zMWJQL4JpN6XdYgBX8LtmTDMVlA1tVbLl4pRJlLu3wWmCN-xMi6a8ojTrouHmKYHytArDYft6d99mgZbi1n2BFLn-CQ4kNdgAGVVLIPGuy1rqPC6BfSGygeHqeMpVa56DZdc05Xpvt9BWE2ao-mt_uDo8lKZqeC4wWvASVaS2qKnHCkKRYtEsR-rb-cQ-HyGNUEBDFLsHyCuNmhbxWVbOEKQPjtEoOkLLCF_jGmosARjfpFZPJ0Kxx8hxX3j5-kpzlHtoxyB59G-TcFlnyjo3oTmv0yrjR88mGEXY6H1fmwv2jsjFu8WF29m6-t5z12eSGQgeOY5hTaQ-wVml8yBxtjaA73XYojyCGKxM-w-Ul7IvTIxgiogbQBiKe_SQq3TIh1NlQSZmbw_JnVz5ORiDrbDaSj9_zyMI-42DSUGixNTNPBs7P54cFVNTbIscM8TqlzUfyaLGp9VRg9ctQcQ81bOyqKcprEC-zwT-Alis_bVxqSQeks7Jhs9OVQ4y5RboubN3GhgrzaqkZSJvlNTgWvzPLdfqHgjKxbnWgUKga8OkrMsfe-YGwxIHqjd29LrMvCck6WeaxB--5CS6LpWf_3UCX5-UFUjGVk3Ynto0ZvGFztkZPAEtJ0v4SpZ9cUhgjw118zo9_z2JaCJLjzJC0vZ1NKJvjYyAS2ri2mT55nGCbe_tGhUCGA1nRTWIZs43mL9hj6D74uaF0vd7oW_G4Mm6HQ1aDF1OyG638M9alV3m1psvUWxBjdS4dn-AgeZc3IAlI4DJpidJS7F8HVvzG1f-v_Hf5hX2kqqM96v6F5qZB-zDyn4SpoykH4099non-lJczVW5qjxnOZu-FWytpXbxwpzLDbL_UlS_GPcRe_gd1ufVZMlfYEZKYqOu7lNhLP4BGPg6QU2xLt2ad-hIVvspAbteZ694A==","summary":[{"type":"summary_text","text":"**Designing elevated-agent integration test**"},{"type":"summary_text","text":"**Designing persistent listener subprocess**"},{"type":"summary_text","text":"**Implementing deelevated sender fixture**"}]},{"id":"rs_017bff223290b7ba016a58b1ed1e98819ab824970bfae59e99","type":"reasoning","content":[],"encrypted_content":"gAAAAABqWLH0RyWnAmA60ZWbkyc_CsDtHveiwS_J3bDOkR2mkWnP1XMTP-7fzrRM4LB4mT05lQtnmVF2mWcUt7XY3jA-gVM4K4TLj51jL7HOmHETtD9bKlWcD0cEs0f4_SwEEq7FZQgRPLOrF36dR68xB8OjKu13G3KYsSAFieCaqJWdDrW4dyo_2OtW31vN4BTkgf4XN_UeJUsDHkCvX-1M2C_tE0S3rbYKxIggbwtxkrvzWBwhcBhk1oATLrHdTgxIUv-zdBOxTBXvRfLYpj0YQ51oSycy-HqGEuQEYii4P1jI2WeR-u9610IpauKWUDUDqGJqkEg3hPrlGHRg2V7wiby5nv2w1u4_zZT0dGTS4pHWCp3F2i1J-E82_LuXZEWKHCS1IiF9hKHkcwdw4irXNPMxa-DYNJbSFkJDV-lo0Nado81WpqeG3DJdb1-qrfPMMIDOw8z3Jy913UYk2eXrP-HU582eqRHOmj1c5brZJtaS2uIFNFDu3k0SdZRLyoqPyUoLybNWvC5M447CZBdEyccmTy307btxRu3tH1B_sgAMuXwkBKrRCQw_rB-k8QoAL-oirrKxfg7xtpcMd7NFzHHJO3YL0uBorNs6Nu3iENPfaQzpEJLzb6lWTXXCnfge1eBV3nSJqHjLb-lDqVl8BTEYM53XbL0CzMTADmGsqYKMWGDfG7bApkOEkNRUVzmbUqx1RyrUCMkjRFHfEmMEqEuHva5dEN_3_y9_RPE5C7Q02jZFbAOBSmXGSYIRGYHuFvwiB_pY6JZTOHaIRFTaakXcHW-JOr_96GJqufZQq7yxHvAD5D0p1zXfsdbFq2pjc5eoCKeYdzp5cLbMudrRBDmXnNCK3cgQyUBG3rSUfYC3mYD77sMJsJc0hMWZksdajJoCilWkHs7q_iZu2XbW8nJX016Swk7vOncznNMcN3oS_YZyonIU6o2pj6mNiVNlThbRo4Na7gWem_sa8-ifpVLVuF3tzpvg5C5iHtOvZEluE89_WDiNl_xVEgEN677pRiYVfe8mKGhLg9qeI6PrQ5aEBZDLC1cXUTGVZZLz88ybffYgyYp5Y7V9xY3ejSPhXQRg365pQia5f7jLGVwPE0tME5j258oX_SBqzeHi6wcJ2dgQCaIIIdtxkAmIhmgvpsuB8-Wml15xrTxngUmb7padmLcYpoOf5oZsNHaH9DKDnoHsJClppBcdykGDxGrKSeOOBA_R6f8nB3v_ldneG2SoWIwQd8Z-BwZG_xFlDr9EbK7SJapU05U3eR0MaQDGGTMLvyQQokl0i3dqpB5lKmeZIO9rPfyPitPmZa4fHoFEITVecq1oBb41FFoAQZZsgyEnXENmVcEDNNB0AjeZMOkNw4WXMsdDsbWn4e-S_FVQF6HMps96k7wsWRBFuP7JEHrG8w1KXyOBQAhj6evTXsrL-Qq48WISaaKOZAOM4wvV0auTy4U5qYdtlwa9I75tEmtHdRY5F6Dc_F44_ibCloZ4vNsLxpYtTjCj5ZIdLf_1zGKRhp6gcq3P7wAf-hcpmViMWNaTQP_kBu9M5wFgM5pmA5T3GMTUyDSCyJNxRjA3034NJSvAC0xsDoZj-TWfZNeHBsdH51MjPQU72yE5dd9RxnbOeX3ohaWYA6jAeQf93nK_hoHGZHqgTz93WjHBtw7jcgYT-eUlUDpMx6C47qrWGcy0nCNon_xJnH5EkzM1EfohbJSE0e-drJIs9XoTeh3mNCeeX3OqC6px8odgZghqVnQyG_w_ZaylELqP7JkZysKUMPPzeqCHR5TYCE3GeFgDHKZBEe1i1X5HvZsotpJ3BeU9_7cuA_fQzt9hHbfiGK9edwL5aiPpxP8PG507wkK18uTw9wjrRvn6UGS3vASgqVFVAgthbScn6wVUNPNHgR6JRmM9wbSKgCIM4l0yon_c3aD7crLKrXTc24f4rODn5MKRSPxQnU9vVDSYiCYhJzeFtV-zh8gZ-Fy9976et_pAa_oXGzCH2b8_hHky6xUfkN6p3ebcIXE6TjIpil6Lc9-E9Q_ZvQ5puX2QnAjWFJJ-W2xWfcboNftZiuZ-LG4Hr3IiW4g_zJ1ldVgPnx0yLys-rXtPTqfwj1EQs65apQQldaZXogUR8KQj2Spoe40Sxg55hQTB-B7xkBvxw2lWoIJ2muCjSTDJVUiM6kpYy1daab9eo9Bl8vQxhUXZt6q29I7DRMEnRfZvQdpWALryWPljW9gIrbFpZcRwuTGzYsrZzhQouUki5cvTiUU_bI9k8uLmA0EGno7hPkmub0Ki9Dsh3oUa_S8GPI0PycmxYlBmi7nl9xdDgdUpa_7vaybeNEtXilggXaQkLjn4masD3of-HgYRFb_VrAN6-qUY1Imtkp1g_EixwyLVqRDZAGON367wAmapHyyZIA3Ae0HQlYglsMMJcwpq673AA_ACpxCd5J8tVu8R2fkcj3zPycHet82mWSTJU0CzzC1iH4vg3-yKsRvTdsY5hDSEcmISetV74F6KMgp8vtuwxLm9HXLHJR-GAhjJdWR3vf-rFnu-oAx7s8G9GU81Hek18RlCc5lPIivc3_5dnH4BPJLnP7CVOSVZJgXzAK2nY-6aCWRd2-Py5gW2zVDS10w2gTcQzoYnmYZeX_uvtaHCPNtKmcjVcrJ_VNKcyRooIrlT77-pKtMFEbeD1aBuRgZuv_oqsYn2MLfF73bixafrOdXCu6dXNbqNqWoCD_HBHdsJvO3TGh3zTyIfvK3scqjqkmRilzFl4e1jEfPngFXQTGmO6_IAvilnktz1X3IAfu_Q08WjSHpXE8HGFukujrsu2hNTGQ2puR9WvkXmCxSeoBiPzv6kMu6PeDYcCQZ7GlactMrpbvMT5HPX1IfdJp6GnHvsiGrsox6fyxH63YsRRPm_JgkDK6bSdR-Zxcoxm-D0Lt57rcoZ8o8jqIuWBQ7ewK-WX8HzYrWCBGSucJZ1MuOnYO3g_-bTResGPCd9jNdEgekL4tbaNrQcpCI3IwFcTYwQk_KUonuZXBru6x4BkjjYJQAa1zqtTV9-UlHr1wYAGiCDJ3V8xah86xs8po6IgvJuUKmq_k-4Xl47022A-NBEjr5bO_UTz66Dkf5eT1cLRK0roF0nVjgKKICqcQ0EWNNdUlloUMDICyFBr7pQ42-zAPIBpUiyBxBvL963ryl9MnDaxpwX0ffrMdvYVfYnk43gENikLL9LS0KmrMAA4NG-1Jr8eFilUK85ojbbBC28WzjujC84mygIRqHKSl8jSClQ_fjjd0-ZQ4NPvxUm64tQ4PQKVOCyN7FH2R28IvFZRKSG00wymkZqm5SBCU-lt1jT41b6aOcxKCMCwZ6oVOs8vOgFEFFjw3C6GWnirFh9Dv4F-Z2uTPpKRBuFpUfX89Sd1shvtzVgrQ3u-Nd58CJbG9cu4fiq1QLPSp8m26GgMqnzSztxOifwAICbzCCHVHoulQEnqp4wKgy-HbOGGm2o3Zly1-TTymZMqIytKQi2E2E1qZcyB1OOMj_cXlxB23ArzuQ4p1LCU-w_2rVA-hey17ISmujuqcqBL9_sFq0t1O6OdawQuSPAmFaCM8AunEct_JDdE-2nhZurxclHLtmGG-T3VX-we3x4twVNTi-UWSe_W1vRo0SJM2JurUHJBZP9G5mtdqADPkJCoacymNozxSpq8oJjlS1fRhLsnUL-GQNkOMgtp8Ef5xtDza-iwoTeFo5YSokzMTLCaCgPeeXtiqq_4wzgs3DpCwUdxQl_q-3zCuQpD_5TNwLidLznVLSHD-vpyZMkwucscYd_WLZHU1D00NnAzg==","summary":[{"type":"summary_text","text":"**Planning event capture via file**"},{"type":"summary_text","text":"**Designing detailed architecture summary**"}]},{"id":"fc_017bff223290b7ba016a58b1f4e79c819ab90b8e81311010bd","type":"function_call","status":"completed","arguments":"{\"type\":null,\"result\":{\"data\":{\"summary\":\"Finding: “elevated endpoint” is not an endpoint state in spt-core. There is no elevation bit in `InfoJson`, the local listener registry, the subnet `Instance`, the spool, WAN message records, or RC/session resolution. The only implemented elevated-agent topology is an externally/harness-hosted elevated process. Its `api listen` child inherits that process context, binds a loopback TCP relay, stamps `controllable = Some(false)`, and can receive messages through the same TCP→relay/stdout or spool→relay/hook paths as any ordinary harness-hosted endpoint. `docs/DEFERRED.md:27` states this explicitly: harness-hosted-elevated “works: TCP listeners + talk-down pipes.” By contrast, `spt rc` attaches only to broker-owned PTY sessions. A harness-hosted endpoint—elevated or not—has no broker session and is intentionally advertised as `harness_only`; RC therefore fails with “no live session” even while messaging remains valid. Spt-hosted endpoints have broker sessions and support RC, but are unelevated by construction because the daemon/broker is forcibly de-elevated (KNOWN-HAZARDS 5.7). Therefore the observed RC failure is explained by hosting topology, not elevated-message incapability. There is no code-grounded evidence that elevation alone blocks message receipt. For a message failure tied to a hyphenated/shortform invocation, the leading explanation is parser/target conflation before routing; the deterministic discriminator is to hold one hyphenated harness-hosted listener constant and compare a direct `spt_msg::deliver::send_windowed` call, canonical `spt send <target>`, and the shortform invocation. Direct+canonical success with shortform failure proves parser conflation; failure of direct delivery proves listener/state/transport instead. No files, endpoints, daemon state, or live `hertz` process were mutated, and no tests were run because this assignment was explicitly read-only.\",\"files\":[{\"path\":\"CONTEXT.md\",\"description\":\"Canonical topology and lifecycle model. Lines 29-39 define daemon/broker/brain and the harness-hosted in-session relay; lines 180-183 define `api bind`, long-running `api listen`, and short-lived `api poll`; lines 232-236 define ReadyAgent/LiveAgent; lines 663-665 define Unbound as broker-session-live but not message-addressable; line 767 mentions an elevated endpoint as a privileged agent process, not as registry state.\"},{\"path\":\"docs/DEFERRED.md\",\"description\":\"Decisive elevation statement at row 27: spt-hosted children are unelevated by construction; an admin-capable agent is harness-hosted-elevated today and “works: TCP listeners + talk-down pipes”; true elevated spt-hosting requires a future consented PTY-host satellite.\"},{\"path\":\"docs/KNOWN-HAZARDS.md\",\"description\":\"KH 2.1/2.5 separate harness-owned PID liveness from daemon-hosted status liveness; KH 4.3 requires stale listener cleanup before address lookup; KH 5.7 requires the daemon always run unelevated in the invoking user universe. This rules out treating an elevated external harness and an elevated daemon/broker as the same thing.\"},{\"path\":\"docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md\",\"description\":\"Exact harness-hosted attachment design: SessionStart writes an adapter-agnostic seed keyed by parent PID; `api listen` consumes it and resolves adapter/profile at bind time from the parent executable/registry.\"},{\"path\":\"docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md\",\"description\":\"Topology split for receive: harness-hosted uses relay-poll; spt-hosted uses a broker-owned translation binary/PTY channel. Also states a poll listener, if present, wins over PTY injection.\"},{\"path\":\"docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md\",\"description\":\"Load-bearing RC invariant: attach is gated on a live broker session, not on perch online/bind state. Unbound is attachable but not message-addressable. This does not make harness-hosted perches attachable because they never have broker sessions.\"},{\"path\":\"docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md\",\"description\":\"Defines message window/channel/persistence axes. Important alternate explanation: `active_only` deliberately avoids the listener wake path, while `force_native` deliberately rejects harness-hosted endpoints because they have no translation-binary PTY channel.\"},{\"path\":\"crates/spt/src/api/startup.rs\",\"description\":\"Exact bind/listen symbols. `bind_from_seed` calls private `establish_perch(..., \\\"live_agent\\\", Some(false), ...)` for harness-hosted endpoints. `establish_perch` writes `InfoJson`, `ready`, session ledger, adapter/cwd/read-env, token—but no elevation metadata. `cmd_listen` resolves the adapter, binds `Relay`, optionally stamps daemon status online, emits `BOUND`/`READY`, drains backlog, and streams events to stdout. `cmd_bind` is the spt-hosted counterpart and stamps `controllable = Some(true)` for live agents.\"},{\"path\":\"crates/spt-store/src/info.rs\",\"description\":\"`InfoJson` authoritative endpoint record. It contains PID/session/type/status/home/adapter/controller/rest/`controllable` and fault fields, but no privilege/elevation/integrity field. `controllable` explicitly means broker-PTY hosting topology: true=spt-hosted, false=harness-hosted.\"},{\"path\":\"crates/spt-msg/src/listener.rs\",\"description\":\"`PollListener`: binds `127.0.0.1:0`, registers `{id,address,pid}` in the local SQLite registry, reads framed `(from,body)`, and unregisters on close/drop. No elevation check, token check, adapter check, or broker dependency is present.\"},{\"path\":\"crates/spt-store/src/registry.rs\",\"description\":\"Local listener registry schema is exactly `agents(id,address,pid,started)`. `register_address`, `resolve_address`, `clean_stale_entries`, and `unregister_address` carry no elevation dimension. Resolution stale-cleans then returns the loopback address.\"},{\"path\":\"crates/spt-daemon/src/relay.rs\",\"description\":\"Harness-hosted receive owner. `Relay::bind` owns a `PollListener`; `drain_backlog` claims non-deferred spool rows; `pump_once` accepts one live TCP message then drains backlog; `run` repeats. It is stateless and respawn-safe.\"},{\"path\":\"crates/spt-msg/src/deliver.rs\",\"description\":\"Local delivery core. `deliver_tcp` resolves the local listener address and counts only a completed framed TCP write as live success. `send_windowed` performs TCP-first except for `active_only`, then durably spools. No elevation branch exists.\"},{\"path\":\"crates/spt/src/cli.rs\",\"description\":\"`Cmd::Send` has a positional `target`; `cmd_send` first tries only the spt-hosted inject leg, then falls back to `deliver::send_windowed`, and finally WAN only on `NoPerch`. A harness-hosted target returns `None` from the inject predicate and therefore proceeds normally to TCP. `--force-native` is the intentional exception: it rejects a harness-hosted/non-controllable endpoint rather than falling back.\"},{\"path\":\"crates/spt-daemon/src/inject.rs\",\"description\":\"`is_spt_hosted_no_relay` is the shared topology discriminator: online, no local relay address, and `InfoJson.controllable == Some(true)`. Unit coverage explicitly proves `Some(false)` harness-hosted endpoints are excluded from PTY injection—not excluded from TCP relay delivery.\"},{\"path\":\"crates/spt-store/src/spool.rs\",\"description\":\"Durable fallback and drain truth. Rows carry `(from,body,window,channel,ephemeral,delivered)` and WAN op dedup. Normal messages use `WINDOW_DEFAULT`; active-only mirrors to `deferred=1`; relay drains non-deferred; hook poll can include deferred; taken rows are marked delivered rather than deleted. No elevation metadata or privilege-conditioned query exists.\"},{\"path\":\"crates/spt/src/api/delivery.rs\",\"description\":\"Short-lived receive path. `cmd_poll` resolves Hook eligibility and emits whole `<EVENT>` envelopes; `poll_drain` claims the active-window rows. This is the fallback/active hook consumer independent of the long-running listener and independent of elevation.\"},{\"path\":\"crates/spt-daemon/src/registryhost.rs\",\"description\":\"`RegistryHost::advertise_local` converts perches to subnet `Instance` rows. It derives `bound` from liveness and `harness_only` from `state == live_agent && controllable != Some(true)`. It advertises status/type/adapter/project/controller facts, but not elevation.\"},{\"path\":\"crates/spt-net/src/net/registry.rs\",\"description\":\"Subnet `Instance` schema carries node/status/epoch/resource/recency/type/bound/controller/`harness_only`/adapter/projects. There is no elevation bit. `harness_only` is explicitly the remote display datum for an online LiveAgent without a broker PTY seat.\"},{\"path\":\"crates/spt/src/wansend.rs\",\"description\":\"Remote route: `wan_send_with` parses the canonical address, resolves a visible `Instance`, refuses own-node stale hits, dials the selected node, sends `WanMessage`, and waits for a receiver acknowledgement. RC uses sibling `resolve_and_dial_owner`, but routing to a node does not create a broker session there.\"},{\"path\":\"crates/spt-daemon/src/wan.rs\",\"description\":\"Remote receive: `receive_wan` applies access and op-id gates, then tries the same local `deliver_tcp` relay path, then spt-hosted translation injection, then an atomic spool+dedup transaction. No elevation condition occurs anywhere in the funnel.\"},{\"path\":\"crates/spt/src/rc.rs\",\"description\":\"Exact RC production path. `run_attach_inner` rejects only explicit offline status, then `establish_attach` connects to the existing broker, calls `resolve_session` against the broker session table, and only on local miss resolves/dials a remote owner. A local registry row owned by this node but absent from the broker session table degrades to `OwnerDial::NotFound`; result is “No live session.” This is expected for every harness-hosted endpoint.\"},{\"path\":\"crates/spt-daemon/src/harnesshost.rs\",\"description\":\"Spt-hosted ownership path: daemon prepares and asks the broker to spawn the adapter’s session command into a broker-held PTY. This is what creates the session RC can attach to. Since the daemon/broker identity is unelevated, these children are unelevated.\"},{\"path\":\"crates/spt-daemon/src/deelevate.rs\",\"description\":\"Daemon privilege invariant and pure seam. `plan_spawn` chooses de-elevation when possible; module docs explicitly say elevated one-shot clients talking downward to an unelevated daemon are valid, while the daemon itself must not inherit elevation.\"},{\"path\":\"crates/spt-store/src/liveness.rs\",\"description\":\"Single liveness resolver. `STATUS_UNBOUND` is broker-attachable but not message-addressable; online daemon-hosted liveness is status-authoritative; external/status-less liveness is PID-based. No elevation dimension.\"},{\"path\":\"crates/spt/tests/contract_e2e.rs\",\"description\":\"Best existing adapter/listener seam. `seed_then_listen_binds_and_relays` already drives real binary `api seed → api listen`, verifies a spooled message reaches stdout, and verifies the bound session/type. It should be extended or mirrored for the isolated discriminator rather than inventing a new harness.\"},{\"path\":\"crates/spt/tests/quickstart_e2e.rs\",\"description\":\"Best existing live-routing seam. Starts a real `spt ready`, invokes canonical `spt send`, asserts `SENT`, and reads the exact `<EVENT>` from receiver stdout; then proves offline spool and restart drain. Missing dimensions are a hyphenated target and privilege/topology comparison.\"},{\"path\":\"crates/spt-proto/src/id.rs\",\"description\":\"Endpoint IDs explicitly permit internal ASCII `-`; tests accept `kebab-case`, `doyle-psyche`, and mixed IDs. Thus a correctly passed hyphenated ID is valid downstream. A failure limited to invocation parsing occurs before this lifecycle.\"},{\"path\":\"crates/spt-proto/src/addr.rs\",\"description\":\"Canonical `[subnet:]id[@node]` parser validates each component through `validate_endpoint_id`; internal hyphens remain legal and unambiguous.\"}],\"architecture\":\"## 1. Production ownership and receive flow\\n\\n```mermaid\\nflowchart LR\\n  EH[Externally launched harness<br/>possibly elevated] -->|SessionStart hook| Seed[api seed<br/>daemon SeedRegistry keyed by parent_pid]\\n  EH -->|starts child| Listen[api listen]\\n  Seed --> Listen\\n  Listen --> Establish[bind_from_seed → establish_perch]\\n  Establish --> Info[info.json + ready + token<br/>controllable=false<br/>NO elevation field]\\n  Listen --> Relay[Relay::bind]\\n  Relay --> PL[PollListener<br/>127.0.0.1:ephemeral]\\n  PL --> LR[(local .registry<br/>id,address,pid,started)]\\n\\n  Send[canonical send / WAN receive] --> Resolve[registry::resolve_address]\\n  Resolve --> TCP[deliver_tcp framed write]\\n  TCP --> Relay\\n  Relay --> Emit[emit canonical EVENT to stdout]\\n  Emit --> EH\\n  TCP -->|miss/refusal| Spool[(perch spool.db)]\\n  Spool -->|relay backlog| Relay\\n  Spool -->|api poll hook| Poll[cmd_poll/poll_drain]\\n  Poll --> EH\\n\\n  RC[spt rc] --> Sessions[broker sessions map]\\n  Sessions -->|harness-hosted: no row| NoSession[No live session]\\n```\\n\\n### Elevation passage\\n\\nThere is intentionally no “elevate endpoint” transition through these stores. Elevation remains an OS property of the externally launched harness/listener process. `establish_perch` serializes topology and identity, not OS privilege. `PollListener` uses loopback TCP and the shared local registry. The daemon is separately forced into the unelevated user universe. Therefore:\\n\\n- **Harness-hosted, ordinary:** external harness owns terminal; listener child; `controllable=false`; messages via TCP/relay or spool/hook; RC unavailable.\\n- **Harness-hosted, elevated:** same persisted/runtime topology as above; the listener inherits the elevated harness context; messages use the identical TCP/relay or spool/hook path; RC unavailable.\\n- **Spt-hosted, ordinary:** broker owns PTY/session; `controllable=true`; messages prefer relay if one exists, otherwise translation-binary injection/spool; RC available.\\n- **Spt-hosted, elevated:** not implemented. The daemon/broker de-elevation invariant makes its children unelevated. A future consented elevation satellite is explicitly deferred.\\n\\nThis also explains an easy observational trap: comparing an ordinary **spt-hosted** endpoint with an elevated **harness-hosted** endpoint changes two variables at once. RC differs because hosting topology differs, not because privilege differs.\\n\\n## 2. Exact call chains\\n\\n### Harness attachment/listener ownership\\n\\n1. Adapter SessionStart invokes `api seed`; daemon holds a `Seed` keyed by stable `parent_pid`.\\n2. Adapter launches `api listen <id>` under the harness.\\n3. `startup::cmd_listen` resolves adapter/profile and calls `bind_from_seed` (or the explicit-session fallback).\\n4. `bind_from_seed → bind_taken_seed → establish_perch(..., \\\"live_agent\\\", Some(false), ...)`.\\n5. `establish_perch` writes `InfoJson { pid = listener process, parent_pid = harness, session_id, state, adapter, controllable=false, ... }`, `ready`, ledger, and capability token. No privilege field is read or written.\\n6. `cmd_listen → Relay::bind → PollListener::bind_blocking` binds `127.0.0.1:0` and calls `registry::register_address(id, addr, current_pid, owlery)`.\\n7. `Relay::run` owns the listener until killed/orphan-watch exit; startup and each cycle drain durable non-deferred backlog.\\n8. `startup::emit` formats delivered `(from,body)` into canonical EVENT lines and flushes stdout into the owning harness session.\\n\\n### Local send\\n\\n1. Clap `Cmd::Send.target: String` is handed to `cli::cmd_send`.\\n2. For non-force-native sends, `cmd_send` may attempt `try_spt_hosted_inject`; a harness-hosted record (`controllable=false`) returns `None`, which is a normal fallthrough, not failure.\\n3. `spt_msg::deliver::send_windowed` checks existing/online perch.\\n4. Unless `active_only`, it calls `deliver_tcp`.\\n5. `deliver_tcp → registry::resolve_address` stale-cleans `.registry`, connects to the registered loopback port, and writes the structural frame. Completed write yields `SENT`.\\n6. A TCP miss writes the message to the per-perch spool with its window/channel/persistence tags and yields `QUEUED`.\\n7. Relay startup/cycles call `drain_non_deferred_audited_at`; hook consumers call `api::delivery::poll_drain`. Thus losing/restarting the listener changes immediate-vs-queued, not deliverability.\\n\\n### WAN send/receive\\n\\n1. Sender `wansend::wan_send_with` parses canonical `Address`, resolves a visible subnet `Instance`, dials its node, writes `WanMessage`, and waits for receiver acknowledgement.\\n2. Receiver dispatcher calls `wan::receive_wan` with the QUIC-proven origin node.\\n3. `receive_wan` applies access, perch-existence, op-id dedup, and user-message restamping.\\n4. It calls the same `deliver_tcp` local relay leg first.\\n5. Only if TCP misses does it try `inject::try_spt_hosted_inject`.\\n6. Any remaining miss atomically commits WAN op-id + spool row. Elevation never participates.\\n\\n### RC\\n\\n1. `rc::run_attach → run_attach_inner → establish_attach` connects to an already-running broker.\\n2. `resolve_session` queries the broker’s endpoint-labeled sessions.\\n3. An spt-hosted endpoint has a broker session; local loopback attach proceeds.\\n4. A harness-hosted endpoint has no broker session. If its subnet registry row resolves back to the same node, `wansend::resolve_and_dial_owner` intentionally returns `NotFound` because local session lookup already missed. Remote routing reaches the owner node but server-side endpoint→session resolution still misses and refuses.\\n5. Result: no live session. This is correct for both ordinary and elevated harness-hosted endpoints and says nothing about TCP listener delivery.\\n\\n## 3. Invariants another agent can test directly\\n\\n1. **No privilege state:** `InfoJson`, local `.registry`, subnet `Instance`, `WanMessage`, and spool schemas have no elevation/integrity field.\\n2. **Topology is explicit:** `controllable == Some(true)` means broker-owned PTY/spt-hosted; `Some(false)` means harness-hosted/no broker PTY. `RegistryHost` publishes the latter as `Instance.harness_only=true`.\\n3. **RC source of truth:** attachability is broker-session existence. Neither `ready`, local listener registration, registry `Active`, nor privilege grants attachability.\\n4. **Live-message success:** a completed framed TCP write is the only relay live-success criterion; privilege is not inspected.\\n5. **Durability:** a TCP miss spools; relay backlog or hook poll claims it later. The listener holds no in-memory message state.\\n6. **Window exception:** `active_only` intentionally does not touch TCP/relay and waits for hook poll. A test using it is not testing ordinary receive.\\n7. **Channel exception:** `force_native` intentionally excludes harness-hosted endpoints; its `leg=cli-gate-not-hosted` is topology truth, not an elevation failure.\\n8. **Spt-hosted elevation is impossible today:** daemon and broker run unelevated; a broker-spawned child inherits that identity.\\n9. **Hyphens are valid downstream:** `validate_endpoint_id` permits `-`, and `Address::parse` preserves an internal hyphen. If the target string reaches these layers correctly, no lifecycle branch rejects it for containing a hyphen.\\n\\n## 4. Ranked falsifiable mechanisms\\n\\n### 1 — RC failure is expected harness-hosted topology, mislabelled as elevation (very high confidence)\\n\\n**Mechanism:** elevated agents are currently externally/harness-hosted. They have `controllable=false` and no broker session, so RC cannot attach.\\n\\n**Falsifier:** show the affected endpoint in `Brain::sessions()` / broker `SessionsReply` under its endpoint ID. If it has a real broker session yet RC says no session, investigate RC resolution/stream lifecycle. An `info.json` with `controllable=false` plus no broker session confirms this hypothesis.\\n\\n**Distinguishing evidence:** ordinary harness-hosted endpoint should fail RC identically; ordinary spt-hosted endpoint should attach. Message delivery may succeed in both.\\n\\n### 2 — Shortform/target parsing never passes the intended ID to canonical send (high for failures limited to a particular spelling; outside this lane’s grammar scope)\\n\\n**Mechanism:** parser conflation occurs before `cmd_send`, so registry/spool/routing never sees the intended target.\\n\\n**Falsifier:** invoke canonical `spt send <exact-id>` or call `send_windowed(<exact-id>,...)` against the same live listener. If both fail, grammar alone is false. If direct/canonical succeeds and shortform fails, grammar is proven.\\n\\n**Distinguishing evidence:** no new spool audit row for the intended perch and no listener frame after shortform, while direct/canonical produces one. Downstream ID validation already accepts internal hyphens.\\n\\n### 3 — Listener never attached or lost ownership (medium)\\n\\n**Mechanism:** `api listen` failed before/at bind (`ADAPTER_UNRESOLVED`, `NO_SEED`, `HOME_REFUSED`, conflict), or its parent-watch exited. Without a live local registry row, normal sends queue rather than emit immediately.\\n\\n**Falsifier:** observe `BOUND:<id>` and `READY:<id>`, `InfoJson.session_id` and `controllable=false`, a current `.registry` row whose PID is alive, plus a successful direct `TcpStream`/`deliver_tcp` write. These establish the complete receive attachment.\\n\\n**Distinguishing evidence:** listener failure affects ordinary and elevated harness-hosted endpoints under the same adapter/bind conditions; elevation is only correlated if it changes parent PID, adapter resolution, or state universe.\\n\\n### 4 — Elevated process and ordinary clients resolve different SPT homes/user universes (medium-low but important on sudo/alternate-admin shapes)\\n\\n**Mechanism:** the endpoint binds under one `$SPT_HOME`/profile while sender and daemon inspect another. On Unix, an independently sudo-launched harness can inherit root’s HOME unless explicitly anchored; on Windows, over-the-shoulder credentials can mean a different user profile. This creates two registries/perch trees, not an elevation-sensitive route.\\n\\n**Falsifier:** compare canonical resolved `spt_home`, owlery path, node identity, `.registry` path, and endpoint `info.json` path from listener and sender. Exact path equality falsifies this.\\n\\n**Distinguishing evidence:** ordinary/elevated records appear in different roots or node IDs. If roots are identical, this mechanism is gone.\\n\\n### 5 — Send modifiers intentionally choose a non-listener channel (medium if modifiers were present, otherwise low)\\n\\n**Mechanism:** `active_only` spools for hook poll and never wakes relay; `force_native` refuses harness-hosted because there is no translation-binary PTY channel.\\n\\n**Falsifier:** repeat with default canonical send. Default success and modifier failure is expected contract behavior.\\n\\n### 6 — Legacy/stale elevated daemon blocks ordinary IPC (low for current code, recognizable fleet-state issue)\\n\\n**Mechanism:** pre-KH-5.7 elevated daemon/named-pipe ownership can deny unelevated clients. This would impair seed/broker commands broadly, not one endpoint’s relay TCP route.\\n\\n**Falsifier:** ordinary endpoints and daemon status/seed/broker IPC all work from the same unelevated client; current daemon PID/token is unelevated.\\n\\n### 7 — OS integrity boundary blocks loopback TCP from ordinary sender to elevated listener (very low)\\n\\n**Mechanism:** hypothetical platform restriction on connecting upward.\\n\\n**Evidence against:** delivery is plain `127.0.0.1` TCP with no integrity/elevation gate, and the accepted deferral explicitly says elevated harness-hosted TCP listeners/talk-down pipes work.\\n\\n**Falsifier/proof:** an elevated listener plus de-elevated sender on the same `SPT_HOME` completes `deliver_tcp` and receives the frame. This is the only rung that directly tests OS token behavior rather than source equivalence.\\n\\n## 5. Minimal deterministic isolated discriminator\\n\\nUse the existing `contract_e2e::seed_then_listen_binds_and_relays` and `quickstart_e2e` patterns; do not touch a live endpoint.\\n\\n### Platform-independent core integration test\\n\\n1. Create a temporary `SPT_HOME`.\\n2. Use a valid hyphenated ID such as `elevated-probe`.\\n3. Start the existing in-process seed daemon fixture.\\n4. Seed a live anchor PID and spawn real binary `spt api --adapter mock listen elevated-probe --parent-pid <anchor>` persistently (not `--once`), or use the explicit `--session-id` fallback.\\n5. Wait for `BOUND:elevated-probe` and `READY:elevated-probe`.\\n6. Assert the written record has the intended ID/session and `controllable == Some(false)`; assert local registry address and owning listener PID exist.\\n7. **Routing baseline:** call `spt_msg::deliver::send_windowed(\\\"elevated-probe\\\", \\\"probe\\\", \\\"direct\\\", owlery, WINDOW_DEFAULT, false)` from the test. Assert `SendOutcome::Sent` and read the exact EVENT from listener stdout.\\n8. **Canonical CLI:** pipe `canonical` to `spt send elevated-probe --from probe`; assert exit 0, `SENT:elevated-probe`, and the second exact EVENT.\\n9. **Shortform candidate:** run the disputed shortform against the still-running same listener and same payload shape. Assert the third EVENT.\\n10. Inspect `spool::audit_rows_at` only if a route queued: it identifies whether the intended perch was reached and which carrier claimed it.\\n11. Separately assert the endpoint has no broker session / is `harness_only`. An RC no-session result alongside successful messages is the expected topology, not a contradiction.\\n\\n**Interpretation matrix:**\\n\\n| Direct core | Canonical CLI | Shortform | Conclusion |\\n|---|---|---|---|\\n| pass | pass | fail | Parser/shortform conflation proven; routing and receive lifecycle healthy |\\n| pass | fail | fail | Canonical Clap/CLI target plumbing issue before delivery |\\n| fail | fail | fail | Listener registry, state universe, liveness, permissions, or transport issue |\\n| queued then backlog emits | queued then backlog emits | varies | Listener was absent/raced, but durable delivery lifecycle works |\\n| messages pass; RC fails | messages pass | any | Harness-hosted topology exactly; elevation not a message blocker |\\n\\nThis is deterministic and hermetic because it never needs UAC/sudo: elevation is deliberately absent from core routing, so the test fixes topology and lexical target independently. It is the minimum test that separates parser from routing without conflating RC.\\n\\n### Optional real-token boundary rung (Windows/Linux acceptance, not ordinary CI)\\n\\nTo directly establish the OS claim, run only in an isolated temp home on a pre-elevated fixture runner:\\n\\n- listener child inherits the elevated test token and binds `elevated-probe`;\\n- sender child is launched under the unelevated linked token / selected sudo invoker against the exact same explicit `SPT_HOME`;\\n- sender uses canonical `spt send`, listener writes the received EVENT to a test-owned file/stdout;\\n- repeat with both children at the same privilege as the control.\\n\\nFour factual observations must be recorded: actual listener and sender token/euid, identical resolved `SPT_HOME`, registered loopback address/PID, and received EVENT. This rung distinguishes a genuine OS integrity/ACL effect from source-level equivalence. It must not be simulated with an environment flag because production has no elevation flag to simulate.\\n\\n## 6. Bottom line\\n\\n- A blanket statement “elevated endpoints cannot receive messages” is unsupported by the implementation and contradicted by the accepted architecture note.\\n- `spt rc` failure is expected for the only currently supported elevated-agent topology: harness-hosted.\\n- The code intentionally models **hosting ownership**, not elevation. Compare like-for-like harness-hosted endpoints before attributing any difference to privilege.\\n- A canonical/direct send to the same hyphenated listener is the decisive non-mutating design seam. If it succeeds while shortform fails, no spool/routing/adapter work is warranted; fix the parser path only.\\n- Actual live elevated-token behavior was not exercised in this read-only investigation, so the runtime statement beyond the accepted doc/source equivalence remains `[INFERENCE]` until the optional isolated token rung is run.\"}}}","call_id":"call_kmrVqVWUmeW3HFpwd8zWLFiY","name":"yield"}]},"duration":168348.26279999968,"ttft":1917.8471999997273,"contextSnapshot":{"promptTokens":229173,"nonMessageTokens":15154}}}
{"type":"custom","customType":"tool_execution_start","data":{"toolCallId":"call_kmrVqVWUmeW3HFpwd8zWLFiY|fc_017bff223290b7ba016a58b1f4e79c819ab90b8e81311010bd","toolName":"yield","startedAt":"2026-07-16T10:28:55.023Z"},"id":"01ad2f12","parentId":"93992d21","timestamp":"2026-07-16T10:28:55.023Z"}
{"type":"message","id":"a9f3afb0","parentId":"01ad2f12","timestamp":"2026-07-16T10:28:55.023Z","message":{"role":"toolResult","toolCallId":"call_kmrVqVWUmeW3HFpwd8zWLFiY|fc_017bff223290b7ba016a58b1f4e79c819ab90b8e81311010bd","toolName":"yield","content":[{"type":"text","text":"Result submitted."}],"details":{"data":{"summary":"Finding: “elevated endpoint” is not an endpoint state in spt-core. There is no elevation bit in `InfoJson`, the local listener registry, the subnet `Instance`, the spool, WAN message records, or RC/session resolution. The only implemented elevated-agent topology is an externally/harness-hosted elevated process. Its `api listen` child inherits that process context, binds a loopback TCP relay, stamps `controllable = Some(false)`, and can receive messages through the same TCP→relay/stdout or spool→relay/hook paths as any ordinary harness-hosted endpoint. `docs/DEFERRED.md:27` states this explicitly: harness-hosted-elevated “works: TCP listeners + talk-down pipes.” By contrast, `spt rc` attaches only to broker-owned PTY sessions. A harness-hosted endpoint—elevated or not—has no broker session and is intentionally advertised as `harness_only`; RC therefore fails with “no live session” even while messaging remains valid. Spt-hosted endpoints have broker sessions and support RC, but are unelevated by construction because the daemon/broker is forcibly de-elevated (KNOWN-HAZARDS 5.7). Therefore the observed RC failure is explained by hosting topology, not elevated-message incapability. There is no code-grounded evidence that elevation alone blocks message receipt. For a message failure tied to a hyphenated/shortform invocation, the leading explanation is parser/target conflation before routing; the deterministic discriminator is to hold one hyphenated harness-hosted listener constant and compare a direct `spt_msg::deliver::send_windowed` call, canonical `spt send <target>`, and the shortform invocation. Direct+canonical success with shortform failure proves parser conflation; failure of direct delivery proves listener/state/transport instead. No files, endpoints, daemon state, or live `hertz` process were mutated, and no tests were run because this assignment was explicitly read-only.","files":[{"path":"CONTEXT.md","description":"Canonical topology and lifecycle model. Lines 29-39 define daemon/broker/brain and the harness-hosted in-session relay; lines 180-183 define `api bind`, long-running `api listen`, and short-lived `api poll`; lines 232-236 define ReadyAgent/LiveAgent; lines 663-665 define Unbound as broker-session-live but not message-addressable; line 767 mentions an elevated endpoint as a privileged agent process, not as registry state."},{"path":"docs/DEFERRED.md","description":"Decisive elevation statement at row 27: spt-hosted children are unelevated by construction; an admin-capable agent is harness-hosted-elevated today and “works: TCP listeners + talk-down pipes”; true elevated spt-hosting requires a future consented PTY-host satellite."},{"path":"docs/KNOWN-HAZARDS.md","description":"KH 2.1/2.5 separate harness-owned PID liveness from daemon-hosted status liveness; KH 4.3 requires stale listener cleanup before address lookup; KH 5.7 requires the daemon always run unelevated in the invoking user universe. This rules out treating an elevated external harness and an elevated daemon/broker as the same thing."},{"path":"docs/adr/0021-harness-hosted-adapter-agnostic-seed-and-bind-time-resolution.md","description":"Exact harness-hosted attachment design: SessionStart writes an adapter-agnostic seed keyed by parent PID; `api listen` consumes it and resolves adapter/profile at bind time from the parent executable/registry."},{"path":"docs/adr/0022-spt-hosted-idle-delivery-translation-binary.md","description":"Topology split for receive: harness-hosted uses relay-poll; spt-hosted uses a broker-owned translation binary/PTY channel. Also states a poll listener, if present, wins over PTY injection."},{"path":"docs/adr/0027-unbound-endpoint-state-and-attach-on-session.md","description":"Load-bearing RC invariant: attach is gated on a live broker session, not on perch online/bind state. Unbound is attachable but not message-addressable. This does not make harness-hosted perches attachable because they never have broker sessions."},{"path":"docs/adr/0028-activity-gated-delivery-and-send-modifier-axes.md","description":"Defines message window/channel/persistence axes. Important alternate explanation: `active_only` deliberately avoids the listener wake path, while `force_native` deliberately rejects harness-hosted endpoints because they have no translation-binary PTY channel."},{"path":"crates/spt/src/api/startup.rs","description":"Exact bind/listen symbols. `bind_from_seed` calls private `establish_perch(..., \"live_agent\", Some(false), ...)` for harness-hosted endpoints. `establish_perch` writes `InfoJson`, `ready`, session ledger, adapter/cwd/read-env, token—but no elevation metadata. `cmd_listen` resolves the adapter, binds `Relay`, optionally stamps daemon status online, emits `BOUND`/`READY`, drains backlog, and streams events to stdout. `cmd_bind` is the spt-hosted counterpart and stamps `controllable = Some(true)` for live agents."},{"path":"crates/spt-store/src/info.rs","description":"`InfoJson` authoritative endpoint record. It contains PID/session/type/status/home/adapter/controller/rest/`controllable` and fault fields, but no privilege/elevation/integrity field. `controllable` explicitly means broker-PTY hosting topology: true=spt-hosted, false=harness-hosted."},{"path":"crates/spt-msg/src/listener.rs","description":"`PollListener`: binds `127.0.0.1:0`, registers `{id,address,pid}` in the local SQLite registry, reads framed `(from,body)`, and unregisters on close/drop. No elevation check, token check, adapter check, or broker dependency is present."},{"path":"crates/spt-store/src/registry.rs","description":"Local listener registry schema is exactly `agents(id,address,pid,started)`. `register_address`, `resolve_address`, `clean_stale_entries`, and `unregister_address` carry no elevation dimension. Resolution stale-cleans then returns the loopback address."},{"path":"crates/spt-daemon/src/relay.rs","description":"Harness-hosted receive owner. `Relay::bind` owns a `PollListener`; `drain_backlog` claims non-deferred spool rows; `pump_once` accepts one live TCP message then drains backlog; `run` repeats. It is stateless and respawn-safe."},{"path":"crates/spt-msg/src/deliver.rs","description":"Local delivery core. `deliver_tcp` resolves the local listener address and counts only a completed framed TCP write as live success. `send_windowed` performs TCP-first except for `active_only`, then durably spools. No elevation branch exists."},{"path":"crates/spt/src/cli.rs","description":"`Cmd::Send` has a positional `target`; `cmd_send` first tries only the spt-hosted inject leg, then falls back to `deliver::send_windowed`, and finally WAN only on `NoPerch`. A harness-hosted target returns `None` from the inject predicate and therefore proceeds normally to TCP. `--force-native` is the intentional exception: it rejects a harness-hosted/non-controllable endpoint rather than falling back."},{"path":"crates/spt-daemon/src/inject.rs","description":"`is_spt_hosted_no_relay` is the shared topology discriminator: online, no local relay address, and `InfoJson.controllable == Some(true)`. Unit coverage explicitly proves `Some(false)` harness-hosted endpoints are excluded from PTY injection—not excluded from TCP relay delivery."},{"path":"crates/spt-store/src/spool.rs","description":"Durable fallback and drain truth. Rows carry `(from,body,window,channel,ephemeral,delivered)` and WAN op dedup. Normal messages use `WINDOW_DEFAULT`; active-only mirrors to `deferred=1`; relay drains non-deferred; hook poll can include deferred; taken rows are marked delivered rather than deleted. No elevation metadata or privilege-conditioned query exists."},{"path":"crates/spt/src/api/delivery.rs","description":"Short-lived receive path. `cmd_poll` resolves Hook eligibility and emits whole `<EVENT>` envelopes; `poll_drain` claims the active-window rows. This is the fallback/active hook consumer independent of the long-running listener and independent of elevation."},{"path":"crates/spt-daemon/src/registryhost.rs","description":"`RegistryHost::advertise_local` converts perches to subnet `Instance` rows. It derives `bound` from liveness and `harness_only` from `state == live_agent && controllable != Some(true)`. It advertises status/type/adapter/project/controller facts, but not elevation."},{"path":"crates/spt-net/src/net/registry.rs","description":"Subnet `Instance` schema carries node/status/epoch/resource/recency/type/bound/controller/`harness_only`/adapter/projects. There is no elevation bit. `harness_only` is explicitly the remote display datum for an online LiveAgent without a broker PTY seat."},{"path":"crates/spt/src/wansend.rs","description":"Remote route: `wan_send_with` parses the canonical address, resolves a visible `Instance`, refuses own-node stale hits, dials the selected node, sends `WanMessage`, and waits for a receiver acknowledgement. RC uses sibling `resolve_and_dial_owner`, but routing to a node does not create a broker session there."},{"path":"crates/spt-daemon/src/wan.rs","description":"Remote receive: `receive_wan` applies access and op-id gates, then tries the same local `deliver_tcp` relay path, then spt-hosted translation injection, then an atomic spool+dedup transaction. No elevation condition occurs anywhere in the funnel."},{"path":"crates/spt/src/rc.rs","description":"Exact RC production path. `run_attach_inner` rejects only explicit offline status, then `establish_attach` connects to the existing broker, calls `resolve_session` against the broker session table, and only on local miss resolves/dials a remote owner. A local registry row owned by this node but absent from the broker session table degrades to `OwnerDial::NotFound`; result is “No live session.” This is expected for every harness-hosted endpoint."},{"path":"crates/spt-daemon/src/harnesshost.rs","description":"Spt-hosted ownership path: daemon prepares and asks the broker to spawn the adapter’s session command into a broker-held PTY. This is what creates the session RC can attach to. Since the daemon/broker identity is unelevated, these children are unelevated."},{"path":"crates/spt-daemon/src/deelevate.rs","description":"Daemon privilege invariant and pure seam. `plan_spawn` chooses de-elevation when possible; module docs explicitly say elevated one-shot clients talking downward to an unelevated daemon are valid, while the daemon itself must not inherit elevation."},{"path":"crates/spt-store/src/liveness.rs","description":"Single liveness resolver. `STATUS_UNBOUND` is broker-attachable but not message-addressable; online daemon-hosted liveness is status-authoritative; external/status-less liveness is PID-based. No elevation dimension."},{"path":"crates/spt/tests/contract_e2e.rs","description":"Best existing adapter/listener seam. `seed_then_listen_binds_and_relays` already drives real binary `api seed → api listen`, verifies a spooled message reaches stdout, and verifies the bound session/type. It should be extended or mirrored for the isolated discriminator rather than inventing a new harness."},{"path":"crates/spt/tests/quickstart_e2e.rs","description":"Best existing live-routing seam. Starts a real `spt ready`, invokes canonical `spt send`, asserts `SENT`, and reads the exact `<EVENT>` from receiver stdout; then proves offline spool and restart drain. Missing dimensions are a hyphenated target and privilege/topology comparison."},{"path":"crates/spt-proto/src/id.rs","description":"Endpoint IDs explicitly permit internal ASCII `-`; tests accept `kebab-case`, `doyle-psyche`, and mixed IDs. Thus a correctly passed hyphenated ID is valid downstream. A failure limited to invocation parsing occurs before this lifecycle."},{"path":"crates/spt-proto/src/addr.rs","description":"Canonical `[subnet:]id[@node]` parser validates each component through `validate_endpoint_id`; internal hyphens remain legal and unambiguous."}],"architecture":"## 1. Production ownership and receive flow\n\n```mermaid\nflowchart LR\n  EH[Externally launched harness<br/>possibly elevated] -->|SessionStart hook| Seed[api seed<br/>daemon SeedRegistry keyed by parent_pid]\n  EH -->|starts child| Listen[api listen]\n  Seed --> Listen\n  Listen --> Establish[bind_from_seed → establish_perch]\n  Establish --> Info[info.json + ready + token<br/>controllable=false<br/>NO elevation field]\n  Listen --> Relay[Relay::bind]\n  Relay --> PL[PollListener<br/>127.0.0.1:ephemeral]\n  PL --> LR[(local .registry<br/>id,address,pid,started)]\n\n  Send[canonical send / WAN receive] --> Resolve[registry::resolve_address]\n  Resolve --> TCP[deliver_tcp framed write]\n  TCP --> Relay\n  Relay --> Emit[emit canonical EVENT to stdout]\n  Emit --> EH\n  TCP -->|miss/refusal| Spool[(perch spool.db)]\n  Spool -->|relay backlog| Relay\n  Spool -->|api poll hook| Poll[cmd_poll/poll_drain]\n  Poll --> EH\n\n  RC[spt rc] --> Sessions[broker sessions map]\n  Sessions -->|harness-hosted: no row| NoSession[No live session]\n```\n\n### Elevation passage\n\nThere is intentionally no “elevate endpoint” transition through these stores. Elevation remains an OS property of the externally launched harness/listener process. `establish_perch` serializes topology and identity, not OS privilege. `PollListener` uses loopback TCP and the shared local registry. The daemon is separately forced into the unelevated user universe. Therefore:\n\n- **Harness-hosted, ordinary:** external harness owns terminal; listener child; `controllable=false`; messages via TCP/relay or spool/hook; RC unavailable.\n- **Harness-hosted, elevated:** same persisted/runtime topology as above; the listener inherits the elevated harness context; messages use the identical TCP/relay or spool/hook path; RC unavailable.\n- **Spt-hosted, ordinary:** broker owns PTY/session; `controllable=true`; messages prefer relay if one exists, otherwise translation-binary injection/spool; RC available.\n- **Spt-hosted, elevated:** not implemented. The daemon/broker de-elevation invariant makes its children unelevated. A future consented elevation satellite is explicitly deferred.\n\nThis also explains an easy observational trap: comparing an ordinary **spt-hosted** endpoint with an elevated **harness-hosted** endpoint changes two variables at once. RC differs because hosting topology differs, not because privilege differs.\n\n## 2. Exact call chains\n\n### Harness attachment/listener ownership\n\n1. Adapter SessionStart invokes `api seed`; daemon holds a `Seed` keyed by stable `parent_pid`.\n2. Adapter launches `api listen <id>` under the harness.\n3. `startup::cmd_listen` resolves adapter/profile and calls `bind_from_seed` (or the explicit-session fallback).\n4. `bind_from_seed → bind_taken_seed → establish_perch(..., \"live_agent\", Some(false), ...)`.\n5. `establish_perch` writes `InfoJson { pid = listener process, parent_pid = harness, session_id, state, adapter, controllable=false, ... }`, `ready`, ledger, and capability token. No privilege field is read or written.\n6. `cmd_listen → Relay::bind → PollListener::bind_blocking` binds `127.0.0.1:0` and calls `registry::register_address(id, addr, current_pid, owlery)`.\n7. `Relay::run` owns the listener until killed/orphan-watch exit; startup and each cycle drain durable non-deferred backlog.\n8. `startup::emit` formats delivered `(from,body)` into canonical EVENT lines and flushes stdout into the owning harness session.\n\n### Local send\n\n1. Clap `Cmd::Send.target: String` is handed to `cli::cmd_send`.\n2. For non-force-native sends, `cmd_send` may attempt `try_spt_hosted_inject`; a harness-hosted record (`controllable=false`) returns `None`, which is a normal fallthrough, not failure.\n3. `spt_msg::deliver::send_windowed` checks existing/online perch.\n4. Unless `active_only`, it calls `deliver_tcp`.\n5. `deliver_tcp → registry::resolve_address` stale-cleans `.registry`, connects to the registered loopback port, and writes the structural frame. Completed write yields `SENT`.\n6. A TCP miss writes the message to the per-perch spool with its window/channel/persistence tags and yields `QUEUED`.\n7. Relay startup/cycles call `drain_non_deferred_audited_at`; hook consumers call `api::delivery::poll_drain`. Thus losing/restarting the listener changes immediate-vs-queued, not deliverability.\n\n### WAN send/receive\n\n1. Sender `wansend::wan_send_with` parses canonical `Address`, resolves a visible subnet `Instance`, dials its node, writes `WanMessage`, and waits for receiver acknowledgement.\n2. Receiver dispatcher calls `wan::receive_wan` with the QUIC-proven origin node.\n3. `receive_wan` applies access, perch-existence, op-id dedup, and user-message restamping.\n4. It calls the same `deliver_tcp` local relay leg first.\n5. Only if TCP misses does it try `inject::try_spt_hosted_inject`.\n6. Any remaining miss atomically commits WAN op-id + spool row. Elevation never participates.\n\n### RC\n\n1. `rc::run_attach → run_attach_inner → establish_attach` connects to an already-running broker.\n2. `resolve_session` queries the broker’s endpoint-labeled sessions.\n3. An spt-hosted endpoint has a broker session; local loopback attach proceeds.\n4. A harness-hosted endpoint has no broker session. If its subnet registry row resolves back to the same node, `wansend::resolve_and_dial_owner` intentionally returns `NotFound` because local session lookup already missed. Remote routing reaches the owner node but server-side endpoint→session resolution still misses and refuses.\n5. Result: no live session. This is correct for both ordinary and elevated harness-hosted endpoints and says nothing about TCP listener delivery.\n\n## 3. Invariants another agent can test directly\n\n1. **No privilege state:** `InfoJson`, local `.registry`, subnet `Instance`, `WanMessage`, and spool schemas have no elevation/integrity field.\n2. **Topology is explicit:** `controllable == Some(true)` means broker-owned PTY/spt-hosted; `Some(false)` means harness-hosted/no broker PTY. `RegistryHost` publishes the latter as `Instance.harness_only=true`.\n3. **RC source of truth:** attachability is broker-session existence. Neither `ready`, local listener registration, registry `Active`, nor privilege grants attachability.\n4. **Live-message success:** a completed framed TCP write is the only relay live-success criterion; privilege is not inspected.\n5. **Durability:** a TCP miss spools; relay backlog or hook poll claims it later. The listener holds no in-memory message state.\n6. **Window exception:** `active_only` intentionally does not touch TCP/relay and waits for hook poll. A test using it is not testing ordinary receive.\n7. **Channel exception:** `force_native` intentionally excludes harness-hosted endpoints; its `leg=cli-gate-not-hosted` is topology truth, not an elevation failure.\n8. **Spt-hosted elevation is impossible today:** daemon and broker run unelevated; a broker-spawned child inherits that identity.\n9. **Hyphens are valid downstream:** `validate_endpoint_id` permits `-`, and `Address::parse` preserves an internal hyphen. If the target string reaches these layers correctly, no lifecycle branch rejects it for containing a hyphen.\n\n## 4. Ranked falsifiable mechanisms\n\n### 1 — RC failure is expected harness-hosted topology, mislabelled as elevation (very high confidence)\n\n**Mechanism:** elevated agents are currently externally/harness-hosted. They have `controllable=false` and no broker session, so RC cannot attach.\n\n**Falsifier:** show the affected endpoint in `Brain::sessions()` / broker `SessionsReply` under its endpoint ID. If it has a real broker session yet RC says no session, investigate RC resolution/stream lifecycle. An `info.json` with `controllable=false` plus no broker session confirms this hypothesis.\n\n**Distinguishing evidence:** ordinary harness-hosted endpoint should fail RC identically; ordinary spt-hosted endpoint should attach. Message delivery may succeed in both.\n\n### 2 — Shortform/target parsing never passes the intended ID to canonical send (high for failures limited to a particular spelling; outside this lane’s grammar scope)\n\n**Mechanism:** parser conflation occurs before `cmd_send`, so registry/spool/routing never sees the intended target.\n\n**Falsifier:** invoke canonical `spt send <exact-id>` or call `send_windowed(<exact-id>,...)` against the same live listener. If both fail, grammar alone is false. If direct/canonical succeeds and shortform fails, grammar is proven.\n\n**Distinguishing evidence:** no new spool audit row for the intended perch and no listener frame after shortform, while direct/canonical produces one. Downstream ID validation already accepts internal hyphens.\n\n### 3 — Listener never attached or lost ownership (medium)\n\n**Mechanism:** `api listen` failed before/at bind (`ADAPTER_UNRESOLVED`, `NO_SEED`, `HOME_REFUSED`, conflict), or its parent-watch exited. Without a live local registry row, normal sends queue rather than emit immediately.\n\n**Falsifier:** observe `BOUND:<id>` and `READY:<id>`, `InfoJson.session_id` and `controllable=false`, a current `.registry` row whose PID is alive, plus a successful direct `TcpStream`/`deliver_tcp` write. These establish the complete receive attachment.\n\n**Distinguishing evidence:** listener failure affects ordinary and elevated harness-hosted endpoints under the same adapter/bind conditions; elevation is only correlated if it changes parent PID, adapter resolution, or state universe.\n\n### 4 — Elevated process and ordinary clients resolve different SPT homes/user universes (medium-low but important on sudo/alternate-admin shapes)\n\n**Mechanism:** the endpoint binds under one `$SPT_HOME`/profile while sender and daemon inspect another. On Unix, an independently sudo-launched harness can inherit root’s HOME unless explicitly anchored; on Windows, over-the-shoulder credentials can mean a different user profile. This creates two registries/perch trees, not an elevation-sensitive route.\n\n**Falsifier:** compare canonical resolved `spt_home`, owlery path, node identity, `.registry` path, and endpoint `info.json` path from listener and sender. Exact path equality falsifies this.\n\n**Distinguishing evidence:** ordinary/elevated records appear in different roots or node IDs. If roots are identical, this mechanism is gone.\n\n### 5 — Send modifiers intentionally choose a non-listener channel (medium if modifiers were present, otherwise low)\n\n**Mechanism:** `active_only` spools for hook poll and never wakes relay; `force_native` refuses harness-hosted because there is no translation-binary PTY channel.\n\n**Falsifier:** repeat with default canonical send. Default success and modifier failure is expected contract behavior.\n\n### 6 — Legacy/stale elevated daemon blocks ordinary IPC (low for current code, recognizable fleet-state issue)\n\n**Mechanism:** pre-KH-5.7 elevated daemon/named-pipe ownership can deny unelevated clients. This would impair seed/broker commands broadly, not one endpoint’s relay TCP route.\n\n**Falsifier:** ordinary endpoints and daemon status/seed/broker IPC all work from the same unelevated client; current daemon PID/token is unelevated.\n\n### 7 — OS integrity boundary blocks loopback TCP from ordinary sender to elevated listener (very low)\n\n**Mechanism:** hypothetical platform restriction on connecting upward.\n\n**Evidence against:** delivery is plain `127.0.0.1` TCP with no integrity/elevation gate, and the accepted deferral explicitly says elevated harness-hosted TCP listeners/talk-down pipes work.\n\n**Falsifier/proof:** an elevated listener plus de-elevated sender on the same `SPT_HOME` completes `deliver_tcp` and receives the frame. This is the only rung that directly tests OS token behavior rather than source equivalence.\n\n## 5. Minimal deterministic isolated discriminator\n\nUse the existing `contract_e2e::seed_then_listen_binds_and_relays` and `quickstart_e2e` patterns; do not touch a live endpoint.\n\n### Platform-independent core integration test\n\n1. Create a temporary `SPT_HOME`.\n2. Use a valid hyphenated ID such as `elevated-probe`.\n3. Start the existing in-process seed daemon fixture.\n4. Seed a live anchor PID and spawn real binary `spt api --adapter mock listen elevated-probe --parent-pid <anchor>` persistently (not `--once`), or use the explicit `--session-id` fallback.\n5. Wait for `BOUND:elevated-probe` and `READY:elevated-probe`.\n6. Assert the written record has the intended ID/session and `controllable == Some(false)`; assert local registry address and owning listener PID exist.\n7. **Routing baseline:** call `spt_msg::deliver::send_windowed(\"elevated-probe\", \"probe\", \"direct\", owlery, WINDOW_DEFAULT, false)` from the test. Assert `SendOutcome::Sent` and read the exact EVENT from listener stdout.\n8. **Canonical CLI:** pipe `canonical` to `spt send elevated-probe --from probe`; assert exit 0, `SENT:elevated-probe`, and the second exact EVENT.\n9. **Shortform candidate:** run the disputed shortform against the still-running same listener and same payload shape. Assert the third EVENT.\n10. Inspect `spool::audit_rows_at` only if a route queued: it identifies whether the intended perch was reached and which carrier claimed it.\n11. Separately assert the endpoint has no broker session / is `harness_only`. An RC no-session result alongside successful messages is the expected topology, not a contradiction.\n\n**Interpretation matrix:**\n\n| Direct core | Canonical CLI | Shortform | Conclusion |\n|---|---|---|---|\n| pass | pass | fail | Parser/shortform conflation proven; routing and receive lifecycle healthy |\n| pass | fail | fail | Canonical Clap/CLI target plumbing issue before delivery |\n| fail | fail | fail | Listener registry, state universe, liveness, permissions, or transport issue |\n| queued then backlog emits | queued then backlog emits | varies | Listener was absent/raced, but durable delivery lifecycle works |\n| messages pass; RC fails | messages pass | any | Harness-hosted topology exactly; elevation not a message blocker |\n\nThis is deterministic and hermetic because it never needs UAC/sudo: elevation is deliberately absent from core routing, so the test fixes topology and lexical target independently. It is the minimum test that separates parser from routing without conflating RC.\n\n### Optional real-token boundary rung (Windows/Linux acceptance, not ordinary CI)\n\nTo directly establish the OS claim, run only in an isolated temp home on a pre-elevated fixture runner:\n\n- listener child inherits the elevated test token and binds `elevated-probe`;\n- sender child is launched under the unelevated linked token / selected sudo invoker against the exact same explicit `SPT_HOME`;\n- sender uses canonical `spt send`, listener writes the received EVENT to a test-owned file/stdout;\n- repeat with both children at the same privilege as the control.\n\nFour factual observations must be recorded: actual listener and sender token/euid, identical resolved `SPT_HOME`, registered loopback address/PID, and received EVENT. This rung distinguishes a genuine OS integrity/ACL effect from source-level equivalence. It must not be simulated with an environment flag because production has no elevation flag to simulate.\n\n## 6. Bottom line\n\n- A blanket statement “elevated endpoints cannot receive messages” is unsupported by the implementation and contradicted by the accepted architecture note.\n- `spt rc` failure is expected for the only currently supported elevated-agent topology: harness-hosted.\n- The code intentionally models **hosting ownership**, not elevation. Compare like-for-like harness-hosted endpoints before attributing any difference to privilege.\n- A canonical/direct send to the same hyphenated listener is the decisive non-mutating design seam. If it succeeds while shortform fails, no spool/routing/adapter work is warranted; fix the parser path only.\n- Actual live elevated-token behavior was not exercised in this read-only investigation, so the runtime statement beyond the accepted doc/source equivalence remains `[INFERENCE]` until the optional isolated token rung is run."},"status":"success"},"isError":false,"timestamp":1784197735023}}
{"type":"message","id":"704048a9","parentId":"a9f3afb0","timestamp":"2026-07-16T10:28:55.234Z","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":1784197735229}}
{"type":"custom","customType":"session_exit","data":{"reason":"dispose","kind":"normal","recordedAt":"2026-07-16T10:35:55.247Z"},"id":"5abc399e","parentId":"704048a9","timestamp":"2026-07-16T10:35:55.247Z"}
